class 模型绑定在 asp.net 核心网络 api 2
Abstract class model binding in asp.net core web api 2
我一直在尝试弄清楚如何将自定义模型绑定与 .net Core 2 web api 结合使用,但一直未能成功。
我已经阅读了以下一些文章
http://www.palmmedia.de/Blog/2018/5/13/aspnet-core-model-binding-of-abstract-classes
Asp net core rc2. Abstract class model binding
在我的例子中,bindingContext.ModelName 总是空的。谁能解释为什么会这样?
下面的示例实现
控制器
public IActionResult SomeAction([ModelBinder(BinderType = typeof(BlahTypeModelBinder))][FromBody]TheBaseClass theBase)
{
return Ok();
}
型号
public abstract class TheBaseClass
{
public abstract int WhatType { get; }
}
public class A : TheBaseClass
{
public override int WhatType { get { return 1; } }
}
public class B : TheBaseClass
{
public override int WhatType { get { return 2; } }
}
提供商
public class BhalTypeBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (context.Metadata.ModelType == typeof(TheBaseClass))
{
var assembly = typeof(TheBaseClass).Assembly;
var abstractSearchClasses = assembly.GetExportedTypes()
.Where(t => t.BaseType.Equals(typeof(TheBaseClass)))
.Where(t => !t.IsAbstract)
.ToList();
var modelBuilderByType = new Dictionary<Type, Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder>();
foreach (var type in abstractSearchClasses)
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
var metadata = context.MetadataProvider.GetMetadataForType(type);
foreach (var property in metadata.Properties)
{
propertyBinders.Add(property, context.CreateBinder(property));
}
modelBuilderByType.Add(type, new Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder(propertyBinders));
}
return new BlahTypeModelBinder(modelBuilderByType, context.MetadataProvider);
}
return null;
}
}
活页夹
public class BlahTypeModelBinder : IModelBinder
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly IDictionary<Type, Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder> _binders;
public BlahTypeModelBinder(IDictionary<Type, Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder> binders, IModelMetadataProvider metadataProvider)
{
_metadataProvider = metadataProvider;
_binders = binders;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var modelTypeValue = bindingContext.ValueProvider.GetValue(ModelNames.CreatePropertyModelName(bindingContext.ModelName, "WhatType"));
if (modelTypeValue != null && modelTypeValue.FirstValue != null)
{
Type modelType = Type.GetType(modelTypeValue.FirstValue);
if (this._binders.TryGetValue(modelType, out var modelBinder))
{
ModelBindingContext innerModelBindingContext = DefaultModelBindingContext.CreateBindingContext(
bindingContext.ActionContext,
bindingContext.ValueProvider,
this._metadataProvider.GetMetadataForType(modelType),
null,
bindingContext.ModelName);
/*modelBinder*/
this._binders.First().Value.BindModelAsync(innerModelBindingContext);
bindingContext.Result = innerModelBindingContext.Result;
return Task.CompletedTask;
}
}
//More code
}
}
您链接的示例使用外部查询字符串参数来确定类型。
如果您像这样调用您的操作:SomeAction?WhatType=YourNamespaceName.A
绑定会按预期工作。
bindingContext.ModelName
为空即可,模型绑定后设置。如果需要,您可以在设置 bindingContext.Result 后设置它。参数WhatType
来自QueryStringValueProvider
,所以没有前缀是可以的。
如何实现仅基于JSON的抽象模型绑定
为此,我们需要:
- 一个值提供程序,用于读取 JSON 并为我们提供一些 "WhatType" 值,代替 QueryStringValueProvider。
- 将提取的数字映射到
Type
-s 的一些反射。
1。价值提供者
这里有一篇关于创建ValueProvider的详细文章:
这里的起点是一些成功从正文中提取 WhatType 整数的代码 json:
public class BlahValueProvider : IValueProvider
{
private readonly string _requestBody;
public BlahValueProvider(string requestBody)
{
_requestBody = requestBody;
}
private const string PROPERTY_NAME = "WhatType";
public bool ContainsPrefix(string prefix)
{
return prefix == PROPERTY_NAME;
}
public ValueProviderResult GetValue(string key)
{
if (key != PROPERTY_NAME)
return ValueProviderResult.None;
// parse json
try
{
var json = JObject.Parse(_requestBody);
return new ValueProviderResult(json.Value<int>("WhatType").ToString());
}
catch (Exception e)
{
// TODO: error handling
throw;
}
}
}
public class BlahValueProviderFactory : IValueProviderFactory
{
public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var request = context.ActionContext.HttpContext.Request;
if (request.ContentType == "application/json")
{
return AddValueProviderAsync(context);
}
return Task.CompletedTask;
}
private Task AddValueProviderAsync(ValueProviderFactoryContext context)
{
using (StreamReader sr = new StreamReader(context.ActionContext.HttpContext.Request.Body))
{
string bodyString = sr.ReadToEnd();
context.ValueProviders.Add(new BlahValueProvider(bodyString));
}
return Task.CompletedTask;
}
}
当然你必须在Startup.cs中注册这个工厂,就像你注册模型绑定器一样。这绝对错过了将提取的数字转换为实际类型(为此,请参见下面的第 2 点),但是如果您在以 if (modelTypeValue != null
开头的行上放置一个断点,您可以看到 modelTypeValue 现在就在那里,即使没有单独的 GET 参数。
2。反射
意识到您正试图根据 在现有实例 上动态计算的 属性 来确定类型(它们不是静态的)。虽然通过了解当前的实现我知道这是可能的(创建模型的空实例,检查 WhatType
属性,丢弃实例),这是非常糟糕的做法,因为没有任何保证实例 属性 是静态常量。
对此的干净解决方案是属性,其中包含 class 的 WhatType 编号。然后我们可以反映该属性并构建一个将 int
s 映射到 Type
s 的映射。这超出了这个问题的范围,但是如果您不熟悉,请查找任何自定义属性教程,您将能够非常快速地将它们组合在一起。
我终于设法解决了这个问题。您不需要提供商。只有以下活页夹有效
public class BlahTypeModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var json = ExtractRequestJson(bindingContext.ActionContext);
var jObject = Newtonsoft.Json.Linq.JObject.Parse(json);
var whatTypeInt = (int)jObject.SelectToken("WhatType");
if (whatTypeInt == 1)
{
var obj = DeserializeObject<A>(json);
bindingContext.Result = ModelBindingResult.Success(obj);
}
else if (whatTypeInt == 2)
{
var obj = DeserializeObject<B>(json);
bindingContext.Result = ModelBindingResult.Success(obj);
}
else
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
return Task.CompletedTask;
}
private static string ExtractRequestJson(ActionContext actionContext)
{
var content = actionContext.HttpContext.Request.Body;
return new StreamReader(content).ReadToEnd();
}
private static T DeserializeObject<T>(string json)
{
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(json, new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Auto
});
}
}
我一直在尝试弄清楚如何将自定义模型绑定与 .net Core 2 web api 结合使用,但一直未能成功。
我已经阅读了以下一些文章 http://www.palmmedia.de/Blog/2018/5/13/aspnet-core-model-binding-of-abstract-classes Asp net core rc2. Abstract class model binding
在我的例子中,bindingContext.ModelName 总是空的。谁能解释为什么会这样?
下面的示例实现
控制器
public IActionResult SomeAction([ModelBinder(BinderType = typeof(BlahTypeModelBinder))][FromBody]TheBaseClass theBase)
{
return Ok();
}
型号
public abstract class TheBaseClass
{
public abstract int WhatType { get; }
}
public class A : TheBaseClass
{
public override int WhatType { get { return 1; } }
}
public class B : TheBaseClass
{
public override int WhatType { get { return 2; } }
}
提供商
public class BhalTypeBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (context.Metadata.ModelType == typeof(TheBaseClass))
{
var assembly = typeof(TheBaseClass).Assembly;
var abstractSearchClasses = assembly.GetExportedTypes()
.Where(t => t.BaseType.Equals(typeof(TheBaseClass)))
.Where(t => !t.IsAbstract)
.ToList();
var modelBuilderByType = new Dictionary<Type, Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder>();
foreach (var type in abstractSearchClasses)
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
var metadata = context.MetadataProvider.GetMetadataForType(type);
foreach (var property in metadata.Properties)
{
propertyBinders.Add(property, context.CreateBinder(property));
}
modelBuilderByType.Add(type, new Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder(propertyBinders));
}
return new BlahTypeModelBinder(modelBuilderByType, context.MetadataProvider);
}
return null;
}
}
活页夹
public class BlahTypeModelBinder : IModelBinder
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly IDictionary<Type, Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder> _binders;
public BlahTypeModelBinder(IDictionary<Type, Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder> binders, IModelMetadataProvider metadataProvider)
{
_metadataProvider = metadataProvider;
_binders = binders;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var modelTypeValue = bindingContext.ValueProvider.GetValue(ModelNames.CreatePropertyModelName(bindingContext.ModelName, "WhatType"));
if (modelTypeValue != null && modelTypeValue.FirstValue != null)
{
Type modelType = Type.GetType(modelTypeValue.FirstValue);
if (this._binders.TryGetValue(modelType, out var modelBinder))
{
ModelBindingContext innerModelBindingContext = DefaultModelBindingContext.CreateBindingContext(
bindingContext.ActionContext,
bindingContext.ValueProvider,
this._metadataProvider.GetMetadataForType(modelType),
null,
bindingContext.ModelName);
/*modelBinder*/
this._binders.First().Value.BindModelAsync(innerModelBindingContext);
bindingContext.Result = innerModelBindingContext.Result;
return Task.CompletedTask;
}
}
//More code
}
}
您链接的示例使用外部查询字符串参数来确定类型。
如果您像这样调用您的操作:SomeAction?WhatType=YourNamespaceName.A
绑定会按预期工作。
bindingContext.ModelName
为空即可,模型绑定后设置。如果需要,您可以在设置 bindingContext.Result 后设置它。参数WhatType
来自QueryStringValueProvider
,所以没有前缀是可以的。
如何实现仅基于JSON的抽象模型绑定
为此,我们需要:
- 一个值提供程序,用于读取 JSON 并为我们提供一些 "WhatType" 值,代替 QueryStringValueProvider。
- 将提取的数字映射到
Type
-s 的一些反射。
1。价值提供者
这里有一篇关于创建ValueProvider的详细文章:
这里的起点是一些成功从正文中提取 WhatType 整数的代码 json:
public class BlahValueProvider : IValueProvider
{
private readonly string _requestBody;
public BlahValueProvider(string requestBody)
{
_requestBody = requestBody;
}
private const string PROPERTY_NAME = "WhatType";
public bool ContainsPrefix(string prefix)
{
return prefix == PROPERTY_NAME;
}
public ValueProviderResult GetValue(string key)
{
if (key != PROPERTY_NAME)
return ValueProviderResult.None;
// parse json
try
{
var json = JObject.Parse(_requestBody);
return new ValueProviderResult(json.Value<int>("WhatType").ToString());
}
catch (Exception e)
{
// TODO: error handling
throw;
}
}
}
public class BlahValueProviderFactory : IValueProviderFactory
{
public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var request = context.ActionContext.HttpContext.Request;
if (request.ContentType == "application/json")
{
return AddValueProviderAsync(context);
}
return Task.CompletedTask;
}
private Task AddValueProviderAsync(ValueProviderFactoryContext context)
{
using (StreamReader sr = new StreamReader(context.ActionContext.HttpContext.Request.Body))
{
string bodyString = sr.ReadToEnd();
context.ValueProviders.Add(new BlahValueProvider(bodyString));
}
return Task.CompletedTask;
}
}
当然你必须在Startup.cs中注册这个工厂,就像你注册模型绑定器一样。这绝对错过了将提取的数字转换为实际类型(为此,请参见下面的第 2 点),但是如果您在以 if (modelTypeValue != null
开头的行上放置一个断点,您可以看到 modelTypeValue 现在就在那里,即使没有单独的 GET 参数。
2。反射
意识到您正试图根据 在现有实例 上动态计算的 属性 来确定类型(它们不是静态的)。虽然通过了解当前的实现我知道这是可能的(创建模型的空实例,检查 WhatType
属性,丢弃实例),这是非常糟糕的做法,因为没有任何保证实例 属性 是静态常量。
对此的干净解决方案是属性,其中包含 class 的 WhatType 编号。然后我们可以反映该属性并构建一个将 int
s 映射到 Type
s 的映射。这超出了这个问题的范围,但是如果您不熟悉,请查找任何自定义属性教程,您将能够非常快速地将它们组合在一起。
我终于设法解决了这个问题。您不需要提供商。只有以下活页夹有效
public class BlahTypeModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var json = ExtractRequestJson(bindingContext.ActionContext);
var jObject = Newtonsoft.Json.Linq.JObject.Parse(json);
var whatTypeInt = (int)jObject.SelectToken("WhatType");
if (whatTypeInt == 1)
{
var obj = DeserializeObject<A>(json);
bindingContext.Result = ModelBindingResult.Success(obj);
}
else if (whatTypeInt == 2)
{
var obj = DeserializeObject<B>(json);
bindingContext.Result = ModelBindingResult.Success(obj);
}
else
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
return Task.CompletedTask;
}
private static string ExtractRequestJson(ActionContext actionContext)
{
var content = actionContext.HttpContext.Request.Body;
return new StreamReader(content).ReadToEnd();
}
private static T DeserializeObject<T>(string json)
{
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(json, new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Auto
});
}
}