我在MVC 6 RC1到RC2迁移后遇到了一些奇怪的行为。
我们假设我们有一个表格的愚蠢版本,会在提交时发布操作:
@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; }
}
我们正在发布的动作,看起来像这样
[HttpPost]
public IActionResult Create(int companyId, InstitutionViewModel institution)
{
...
}
我遇到的问题是提交永远不会触发操作。浏览器显示微调器,后台发生了一些事情,但程序从未到达该操作。更糟糕的是 - 当发生这种情况时,dotnet进程的 RAM消耗开始逐渐上升,直到它用完为止。我最后一次让网站在这种状态下运行,dotnet进程使用的是7GB内存,只需要2到3分钟就可以达到这一点!
这曾经在RC1中没有任何问题。到目前为止,我找到的唯一解决方案是从InstitutionViewModel中删除LocationViewModel属性。如果我这样做,POST会毫无问题地完成操作。
LocationViewModel本身似乎也不是问题,因为如果类中有任何其他viewModel作为属性,则会发生同样的情况,无论viewModel包含什么。
现在我感到困惑,这是RC2中的一个错误,或者我做了一些可怕的错误。也许我忘了包含一些东西,或者在升级到RC2时我在Startup.cs和project.json中破坏了一些东西。有没有人有任何想法?
答案 0 :(得分:3)
现在我很困惑天气这是RC2中的一个错误,或者我做了一些可怕的错误。
它的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>();