表单帖子永远不会执行操作,并且在MVC 6 RC2中存在内存错误

时间:2016-06-03 12:25:34

标签: asp.net-mvc memory-leaks asp.net-core-mvc .net-core-rc2

我在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分钟就可以达到这一点! enter image description here

这曾经在RC1中没有任何问题。到目前为止,我找到的唯一解决方案是从InstitutionViewModel中删除LocationViewModel属性。如果我这样做,POST会毫无问题地完成操作。

LocationViewModel本身似乎也不是问题,因为如果类中有任何其他viewModel作为属性,则会发生同样的情况,无论viewModel包含什么。

现在我感到困惑,这是RC2中的一个错误,或者我做了一些可怕的错误。也许我忘了包含一些东西,或者在升级到RC2时我在Startup.cs和project.json中破坏了一些东西。有没有人有任何想法?

1 个答案:

答案 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

的DI容器中注册
services.AddSingleton<IModelBinderFactory, MyModelBinderFactory>();