Skip to content

Instantly share code, notes, and snippets.

@rynowak
Created May 18, 2016 20:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rynowak/9488b3b1a6b81876d8b14de72feb0857 to your computer and use it in GitHub Desktop.
Save rynowak/9488b3b1a6b81876d8b14de72feb0857 to your computer and use it in GitHub Desktop.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// A factory for <see cref="IModelBinder"/> instances.
/// </summary>
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}')";
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment