表单 post 从不执行操作并且在 MVC 6 RC2 中存在内存错误
Form post never executes action and has a memory bug in MVC 6 RC2
我在 MVC 6 RC1 到 RC2 迁移后遇到了一些奇怪的行为。
假设我们有一个简化版本的表单,它将 POST 在提交时执行一个操作:
@model InstitutionViewModel
<form asp-controller="Institution" asp-action="Create" method="post">
@Html.Hidden("companyId", ViewBag.CompanyId)
@Html.DropDownListFor(Model => Model.LocationId, (List<SelectListItem>)ViewBag.Locations, new { Class = "form-control" })
@Html.TextAreaFor(model => model.Description, new { Class = "form-control" })
<input type="submit" value="Submit" class="btn btn-success" />
</form>
然后我们有这个 InstitutionViewModel
public class InstitutionViewModel
{
public int Id { get; set; }
public string Description { get; set; }
public int LocationId { get; set; }
public LocationViewModel Location { get; set; }
}
我们POST要执行的操作看起来像这样
[HttpPost]
public IActionResult Create(int companyId, InstitutionViewModel institution)
{
...
}
我遇到的问题是提交永远不会触发操作。浏览器正在显示微调器并且后台正在发生某些事情,但程序从未到达动作。更糟糕的是 - 当这种情况发生时,dotnet 进程的 RAM 消耗开始逐渐上升,直到它刚好 运行 耗尽。上次我让网站 运行 处于这种状态时,dotnet 进程使用了 7GB 的 RAM,并且只用了大约 2 或 3 分钟就达到了这一点!
这在 RC1 中曾经没有任何问题。到目前为止,我为此找到的唯一解决方案是从 InstitutionViewModel 中删除 LocationViewModel 属性。如果我这样做,POST 会毫无问题地执行操作。
LocationViewModel 本身似乎也不是问题,因为如果 class 中有任何其他 viewModel 作为 属性,无论 viewModel 包含什么,都会发生同样的情况。
现在我很困惑这是 RC2 中的错误还是我做错了什么。也许我忘了包含某些东西,或者我在升级到 RC2 时在 Startup.cs 和 project.json 中破坏了一些东西。有人有什么想法吗?
Now I'm confused weather this is a bug in RC2 or I'm doing something horribly wrong.
这是a known bug in ASP.NET Core MVC RC2,由于在默认模型绑定器工厂中对深层嵌套模型的处理不正确。
推荐的解决方法是 to use a custom binder factory until it is fixed:
public class MyModelBinderFactory : IModelBinderFactory
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly IModelBinderProvider[] _providers;
private readonly ConcurrentDictionary<object, IModelBinder> _cache;
/// <summary>
/// Creates a new <see cref="ModelBinderFactory"/>.
/// </summary>
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
/// <param name="options">The <see cref="IOptions{TOptions}"/> for <see cref="MvcOptions"/>.</param>
public MyModelBinderFactory(IModelMetadataProvider metadataProvider, IOptions<MvcOptions> options)
{
_metadataProvider = metadataProvider;
_providers = options.Value.ModelBinderProviders.ToArray();
_cache = new ConcurrentDictionary<object, IModelBinder>();
}
/// <inheritdoc />
public IModelBinder CreateBinder(ModelBinderFactoryContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// We perform caching in CreateBinder (not in CreateBinderCore) because we only want to
// cache the top-level binder.
IModelBinder binder;
if (context.CacheToken != null && _cache.TryGetValue(context.CacheToken, out binder))
{
return binder;
}
var providerContext = new DefaultModelBinderProviderContext(this, context);
binder = CreateBinderCore(providerContext, context.CacheToken);
if (binder == null)
{
var message = $"Could not create model binder for {providerContext.Metadata.ModelType}.";
throw new InvalidOperationException(message);
}
if (context.CacheToken != null)
{
_cache.TryAdd(context.CacheToken, binder);
}
return binder;
}
private IModelBinder CreateBinderCore(DefaultModelBinderProviderContext providerContext, object token)
{
if (!providerContext.Metadata.IsBindingAllowed)
{
return NoOpBinder.Instance;
}
// A non-null token will usually be passed in at the the top level (ParameterDescriptor likely).
// This prevents us from treating a parameter the same as a collection-element - which could
// happen looking at just model metadata.
var key = new Key(providerContext.Metadata, token);
// If we're currently recursively building a binder for this type, just return
// a PlaceholderBinder. We'll fix it up later to point to the 'real' binder
// when the stack unwinds.
var collection = providerContext.Collection;
IModelBinder binder;
if (collection.TryGetValue(key, out binder))
{
if (binder != null)
{
return binder;
}
// Recursion detected, create a DelegatingBinder.
binder = new PlaceholderBinder();
collection[key] = binder;
return binder;
}
// OK this isn't a recursive case (yet) so "push" an entry on the stack and then ask the providers
// to create the binder.
collection.Add(key, null);
IModelBinder result = null;
for (var i = 0; i < _providers.Length; i++)
{
var provider = _providers[i];
result = provider.GetBinder(providerContext);
if (result != null)
{
break;
}
}
if (result == null && token == null)
{
// Use a no-op binder if we're below the top level. At the top level, we throw.
result = NoOpBinder.Instance;
}
// If the DelegatingBinder was created, then it means we recursed. Hook it up to the 'real' binder.
var delegatingBinder = collection[key] as PlaceholderBinder;
if (delegatingBinder != null)
{
delegatingBinder.Inner = result;
}
collection[key] = result;
return result;
}
private class DefaultModelBinderProviderContext : ModelBinderProviderContext
{
private readonly MyModelBinderFactory _factory;
public DefaultModelBinderProviderContext(
MyModelBinderFactory factory,
ModelBinderFactoryContext factoryContext)
{
_factory = factory;
Metadata = factoryContext.Metadata;
BindingInfo = factoryContext.BindingInfo;
MetadataProvider = _factory._metadataProvider;
Collection = new Dictionary<Key, IModelBinder>();
}
private DefaultModelBinderProviderContext(
DefaultModelBinderProviderContext parent,
ModelMetadata metadata)
{
Metadata = metadata;
_factory = parent._factory;
MetadataProvider = parent.MetadataProvider;
Collection = parent.Collection;
BindingInfo = new BindingInfo()
{
BinderModelName = metadata.BinderModelName,
BinderType = metadata.BinderType,
BindingSource = metadata.BindingSource,
PropertyFilterProvider = metadata.PropertyFilterProvider,
};
}
public override BindingInfo BindingInfo { get; }
public override ModelMetadata Metadata { get; }
public override IModelMetadataProvider MetadataProvider { get; }
// Not using a 'real' Stack<> because we want random access to modify the entries.
public Dictionary<Key, IModelBinder> Collection { get; }
public override IModelBinder CreateBinder(ModelMetadata metadata)
{
var nestedContext = new DefaultModelBinderProviderContext(this, metadata);
return _factory.CreateBinderCore(nestedContext, token: null);
}
}
[DebuggerDisplay("{ToString(),nq}")]
private struct Key : IEquatable<Key>
{
private readonly ModelMetadata _metadata;
private readonly object _token; // Explicitly using ReferenceEquality for tokens.
public Key(ModelMetadata metadata, object token)
{
_metadata = metadata;
_token = token;
}
public bool Equals(Key other)
{
return _metadata.Equals(other._metadata) && object.ReferenceEquals(_token, other._token);
}
public override bool Equals(object obj)
{
var other = obj as Key?;
return other.HasValue && Equals(other.Value);
}
public override int GetHashCode()
{
return _metadata.GetHashCode() ^ RuntimeHelpers.GetHashCode(_token);
}
public override string ToString()
{
if (_metadata.MetadataKind == ModelMetadataKind.Type)
{
return $"{_token} (Type: '{_metadata.ModelType.Name}')";
}
else
{
return $"{_token} (Property: '{_metadata.ContainerType.Name}.{_metadata.PropertyName}' Type: '{_metadata.ModelType.Name}')";
}
}
}
}
您可以从 Startup.ConfigureServices
:
将其注册到 DI 容器中
services.AddSingleton<IModelBinderFactory, MyModelBinderFactory>();
我在 MVC 6 RC1 到 RC2 迁移后遇到了一些奇怪的行为。
假设我们有一个简化版本的表单,它将 POST 在提交时执行一个操作:
@model InstitutionViewModel
<form asp-controller="Institution" asp-action="Create" method="post">
@Html.Hidden("companyId", ViewBag.CompanyId)
@Html.DropDownListFor(Model => Model.LocationId, (List<SelectListItem>)ViewBag.Locations, new { Class = "form-control" })
@Html.TextAreaFor(model => model.Description, new { Class = "form-control" })
<input type="submit" value="Submit" class="btn btn-success" />
</form>
然后我们有这个 InstitutionViewModel
public class InstitutionViewModel
{
public int Id { get; set; }
public string Description { get; set; }
public int LocationId { get; set; }
public LocationViewModel Location { get; set; }
}
我们POST要执行的操作看起来像这样
[HttpPost]
public IActionResult Create(int companyId, InstitutionViewModel institution)
{
...
}
我遇到的问题是提交永远不会触发操作。浏览器正在显示微调器并且后台正在发生某些事情,但程序从未到达动作。更糟糕的是 - 当这种情况发生时,dotnet 进程的 RAM 消耗开始逐渐上升,直到它刚好 运行 耗尽。上次我让网站 运行 处于这种状态时,dotnet 进程使用了 7GB 的 RAM,并且只用了大约 2 或 3 分钟就达到了这一点!
这在 RC1 中曾经没有任何问题。到目前为止,我为此找到的唯一解决方案是从 InstitutionViewModel 中删除 LocationViewModel 属性。如果我这样做,POST 会毫无问题地执行操作。
LocationViewModel 本身似乎也不是问题,因为如果 class 中有任何其他 viewModel 作为 属性,无论 viewModel 包含什么,都会发生同样的情况。
现在我很困惑这是 RC2 中的错误还是我做错了什么。也许我忘了包含某些东西,或者我在升级到 RC2 时在 Startup.cs 和 project.json 中破坏了一些东西。有人有什么想法吗?
Now I'm confused weather this is a bug in RC2 or I'm doing something horribly wrong.
这是a known bug in ASP.NET Core MVC RC2,由于在默认模型绑定器工厂中对深层嵌套模型的处理不正确。
推荐的解决方法是 to use a custom binder factory until it is fixed:
public class MyModelBinderFactory : IModelBinderFactory
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly IModelBinderProvider[] _providers;
private readonly ConcurrentDictionary<object, IModelBinder> _cache;
/// <summary>
/// Creates a new <see cref="ModelBinderFactory"/>.
/// </summary>
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
/// <param name="options">The <see cref="IOptions{TOptions}"/> for <see cref="MvcOptions"/>.</param>
public MyModelBinderFactory(IModelMetadataProvider metadataProvider, IOptions<MvcOptions> options)
{
_metadataProvider = metadataProvider;
_providers = options.Value.ModelBinderProviders.ToArray();
_cache = new ConcurrentDictionary<object, IModelBinder>();
}
/// <inheritdoc />
public IModelBinder CreateBinder(ModelBinderFactoryContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// We perform caching in CreateBinder (not in CreateBinderCore) because we only want to
// cache the top-level binder.
IModelBinder binder;
if (context.CacheToken != null && _cache.TryGetValue(context.CacheToken, out binder))
{
return binder;
}
var providerContext = new DefaultModelBinderProviderContext(this, context);
binder = CreateBinderCore(providerContext, context.CacheToken);
if (binder == null)
{
var message = $"Could not create model binder for {providerContext.Metadata.ModelType}.";
throw new InvalidOperationException(message);
}
if (context.CacheToken != null)
{
_cache.TryAdd(context.CacheToken, binder);
}
return binder;
}
private IModelBinder CreateBinderCore(DefaultModelBinderProviderContext providerContext, object token)
{
if (!providerContext.Metadata.IsBindingAllowed)
{
return NoOpBinder.Instance;
}
// A non-null token will usually be passed in at the the top level (ParameterDescriptor likely).
// This prevents us from treating a parameter the same as a collection-element - which could
// happen looking at just model metadata.
var key = new Key(providerContext.Metadata, token);
// If we're currently recursively building a binder for this type, just return
// a PlaceholderBinder. We'll fix it up later to point to the 'real' binder
// when the stack unwinds.
var collection = providerContext.Collection;
IModelBinder binder;
if (collection.TryGetValue(key, out binder))
{
if (binder != null)
{
return binder;
}
// Recursion detected, create a DelegatingBinder.
binder = new PlaceholderBinder();
collection[key] = binder;
return binder;
}
// OK this isn't a recursive case (yet) so "push" an entry on the stack and then ask the providers
// to create the binder.
collection.Add(key, null);
IModelBinder result = null;
for (var i = 0; i < _providers.Length; i++)
{
var provider = _providers[i];
result = provider.GetBinder(providerContext);
if (result != null)
{
break;
}
}
if (result == null && token == null)
{
// Use a no-op binder if we're below the top level. At the top level, we throw.
result = NoOpBinder.Instance;
}
// If the DelegatingBinder was created, then it means we recursed. Hook it up to the 'real' binder.
var delegatingBinder = collection[key] as PlaceholderBinder;
if (delegatingBinder != null)
{
delegatingBinder.Inner = result;
}
collection[key] = result;
return result;
}
private class DefaultModelBinderProviderContext : ModelBinderProviderContext
{
private readonly MyModelBinderFactory _factory;
public DefaultModelBinderProviderContext(
MyModelBinderFactory factory,
ModelBinderFactoryContext factoryContext)
{
_factory = factory;
Metadata = factoryContext.Metadata;
BindingInfo = factoryContext.BindingInfo;
MetadataProvider = _factory._metadataProvider;
Collection = new Dictionary<Key, IModelBinder>();
}
private DefaultModelBinderProviderContext(
DefaultModelBinderProviderContext parent,
ModelMetadata metadata)
{
Metadata = metadata;
_factory = parent._factory;
MetadataProvider = parent.MetadataProvider;
Collection = parent.Collection;
BindingInfo = new BindingInfo()
{
BinderModelName = metadata.BinderModelName,
BinderType = metadata.BinderType,
BindingSource = metadata.BindingSource,
PropertyFilterProvider = metadata.PropertyFilterProvider,
};
}
public override BindingInfo BindingInfo { get; }
public override ModelMetadata Metadata { get; }
public override IModelMetadataProvider MetadataProvider { get; }
// Not using a 'real' Stack<> because we want random access to modify the entries.
public Dictionary<Key, IModelBinder> Collection { get; }
public override IModelBinder CreateBinder(ModelMetadata metadata)
{
var nestedContext = new DefaultModelBinderProviderContext(this, metadata);
return _factory.CreateBinderCore(nestedContext, token: null);
}
}
[DebuggerDisplay("{ToString(),nq}")]
private struct Key : IEquatable<Key>
{
private readonly ModelMetadata _metadata;
private readonly object _token; // Explicitly using ReferenceEquality for tokens.
public Key(ModelMetadata metadata, object token)
{
_metadata = metadata;
_token = token;
}
public bool Equals(Key other)
{
return _metadata.Equals(other._metadata) && object.ReferenceEquals(_token, other._token);
}
public override bool Equals(object obj)
{
var other = obj as Key?;
return other.HasValue && Equals(other.Value);
}
public override int GetHashCode()
{
return _metadata.GetHashCode() ^ RuntimeHelpers.GetHashCode(_token);
}
public override string ToString()
{
if (_metadata.MetadataKind == ModelMetadataKind.Type)
{
return $"{_token} (Type: '{_metadata.ModelType.Name}')";
}
else
{
return $"{_token} (Property: '{_metadata.ContainerType.Name}.{_metadata.PropertyName}' Type: '{_metadata.ModelType.Name}')";
}
}
}
}
您可以从 Startup.ConfigureServices
:
services.AddSingleton<IModelBinderFactory, MyModelBinderFactory>();