|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
namespace Microsoft.AspNetCore.Mvc.ModelBinding;
public class ModelBinderFactoryTest
{
[Fact]
public void CreateBinder_Throws_WhenNoProviders()
{
// Arrange
var expected = $"'{typeof(MvcOptions).FullName}.{nameof(MvcOptions.ModelBinderProviders)}' must not be " +
$"empty. At least one '{typeof(IModelBinderProvider).FullName}' is required to model bind.";
var metadataProvider = new TestModelMetadataProvider();
var options = Options.Create(new MvcOptions());
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(string)),
};
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => factory.CreateBinder(context));
Assert.Equal(expected, exception.Message);
}
[Fact]
public void CreateBinder_Throws_WhenBinderNotCreated()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = Options.Create(new MvcOptions());
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(_ => null));
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(string)),
};
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => factory.CreateBinder(context));
Assert.Equal(
$"Could not create a model binder for model object of type '{typeof(string).FullName}'.",
exception.Message);
}
[Fact]
public void CreateBinder_CreatesNoOpBinder_WhenPropertyDoesntHaveABinder()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
// There isn't a provider that can handle WidgetId.
var options = Options.Create(new MvcOptions());
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
if (c.Metadata.ModelType == typeof(Widget))
{
Assert.NotNull(c.CreateBinder(c.Metadata.Properties[nameof(Widget.Id)]));
return Mock.Of<IModelBinder>();
}
return null;
}));
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Widget)),
};
// Act
var result = factory.CreateBinder(context);
// Assert
Assert.NotNull(result);
}
[Fact]
public void CreateBinder_CreatesNoOpBinder_WhenPropertyBindingIsNotAllowed()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty<Widget>(nameof(Widget.Id))
.BindingDetails(m => m.IsBindingAllowed = false);
var modelBinder = new ByteArrayModelBinder(NullLoggerFactory.Instance);
var options = Options.Create(new MvcOptions());
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
if (c.Metadata.ModelType == typeof(WidgetId))
{
return modelBinder;
}
return null;
}));
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForProperty(typeof(Widget), nameof(Widget.Id)),
};
// Act
var result = factory.CreateBinder(context);
// Assert
Assert.NotNull(result);
Assert.IsType<NoOpBinder>(result);
}
[Fact]
public void CreateBinder_NestedProperties()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = Options.Create(new MvcOptions());
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
if (c.Metadata.ModelType == typeof(Widget))
{
Assert.NotNull(c.CreateBinder(c.Metadata.Properties[nameof(Widget.Id)]));
return Mock.Of<IModelBinder>();
}
else if (c.Metadata.ModelType == typeof(WidgetId))
{
return Mock.Of<IModelBinder>();
}
return null;
}));
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Widget)),
};
// Act
var result = factory.CreateBinder(context);
// Assert
Assert.NotNull(result);
}
[Fact]
public void CreateBinder_BreaksCycles()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var callCount = 0;
var options = Options.Create(new MvcOptions());
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
var currentCallCount = ++callCount;
Assert.Equal(typeof(Employee), c.Metadata.ModelType);
var binder = c.CreateBinder(c.Metadata.Properties[nameof(Employee.Manager)]);
if (currentCallCount == 2)
{
Assert.IsType<PlaceholderBinder>(binder);
}
return Mock.Of<IModelBinder>();
}));
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Employee)),
};
// Act
var result = factory.CreateBinder(context);
// Assert
Assert.NotNull(result);
}
[Fact]
public void CreateBinder_DoesNotCache_WhenTokenIsNull()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = Options.Create(new MvcOptions());
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
Assert.Equal(typeof(Employee), c.Metadata.ModelType);
return Mock.Of<IModelBinder>();
}));
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Employee)),
};
// Act
var result1 = factory.CreateBinder(context);
var result2 = factory.CreateBinder(context);
// Assert
Assert.NotSame(result1, result2);
}
[Fact]
public void CreateBinder_Caches_WhenTokenIsNotNull()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = Options.Create(new MvcOptions());
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
Assert.Equal(typeof(Employee), c.Metadata.ModelType);
return Mock.Of<IModelBinder>();
}));
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Employee)),
CacheToken = new object(),
};
// Act
var result1 = factory.CreateBinder(context);
var result2 = factory.CreateBinder(context);
// Assert
Assert.Same(result1, result2);
}
public static TheoryData<BindingInfo, BindingMetadata, BindingInfo> BindingInfoData
{
get
{
var propertyFilterProvider = Mock.Of<IPropertyFilterProvider>();
var emptyBindingInfo = new BindingInfo();
var halfBindingInfo = new BindingInfo
{
BinderModelName = "expected name",
BinderType = typeof(WidgetBinder),
};
var fullBindingInfo = new BindingInfo
{
BinderModelName = "expected name",
BinderType = typeof(WidgetBinder),
BindingSource = BindingSource.Services,
PropertyFilterProvider = propertyFilterProvider,
};
var emptyBindingMetadata = new BindingMetadata();
var differentBindingMetadata = new BindingMetadata
{
BinderModelName = "not the expected name",
BinderType = typeof(WidgetIdBinder),
BindingSource = BindingSource.ModelBinding,
PropertyFilterProvider = Mock.Of<IPropertyFilterProvider>(),
};
var secondHalfBindingMetadata = new BindingMetadata
{
BindingSource = BindingSource.Services,
PropertyFilterProvider = propertyFilterProvider,
};
var fullBindingMetadata = new BindingMetadata
{
BinderModelName = "expected name",
BinderType = typeof(WidgetBinder),
BindingSource = BindingSource.Services,
PropertyFilterProvider = propertyFilterProvider,
};
// parameterBindingInfo, bindingMetadata, expectedInfo
return new TheoryData<BindingInfo, BindingMetadata, BindingInfo>
{
{ emptyBindingInfo, emptyBindingMetadata, emptyBindingInfo },
{ fullBindingInfo, emptyBindingMetadata, fullBindingInfo },
{ emptyBindingInfo, fullBindingMetadata, fullBindingInfo },
// Resulting BindingInfo combines two inputs
{ halfBindingInfo, secondHalfBindingMetadata, fullBindingInfo },
// Parameter information has precedence over type metadata
{ fullBindingInfo, differentBindingMetadata, fullBindingInfo },
};
}
}
[Theory]
[MemberData(nameof(BindingInfoData))]
public void CreateBinder_PassesExpectedBindingInfo(
BindingInfo parameterBindingInfo,
BindingMetadata bindingMetadata,
BindingInfo expectedInfo)
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType<Employee>().BindingDetails(binding =>
{
binding.BinderModelName = bindingMetadata.BinderModelName;
binding.BinderType = bindingMetadata.BinderType;
binding.BindingSource = bindingMetadata.BindingSource;
if (bindingMetadata.PropertyFilterProvider != null)
{
binding.PropertyFilterProvider = bindingMetadata.PropertyFilterProvider;
}
});
var modelBinder = Mock.Of<IModelBinder>();
var modelBinderProvider = new TestModelBinderProvider(context =>
{
Assert.Equal(typeof(Employee), context.Metadata.ModelType);
Assert.NotNull(context.BindingInfo);
Assert.Equal(expectedInfo.BinderModelName, context.BindingInfo.BinderModelName, StringComparer.Ordinal);
Assert.Equal(expectedInfo.BinderType, context.BindingInfo.BinderType);
Assert.Equal(expectedInfo.BindingSource, context.BindingInfo.BindingSource);
Assert.Same(expectedInfo.PropertyFilterProvider, context.BindingInfo.PropertyFilterProvider);
return modelBinder;
});
var options = Options.Create(new MvcOptions());
options.Value.ModelBinderProviders.Insert(0, modelBinderProvider);
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var factoryContext = new ModelBinderFactoryContext
{
BindingInfo = parameterBindingInfo,
Metadata = metadataProvider.GetMetadataForType(typeof(Employee)),
};
// Act & Assert
var result = factory.CreateBinder(factoryContext);
// Confirm our IModelBinderProvider was called.
Assert.Same(modelBinder, result);
}
[Fact]
public void CreateBinder_Caches_NonRootNodes()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = Options.Create(new MvcOptions());
IModelBinder inner = null;
var widgetProvider = new TestModelBinderProvider(c =>
{
if (c.Metadata.ModelType == typeof(Widget))
{
var binder = c.CreateBinder(c.Metadata.Properties[nameof(Widget.Id)]);
if (inner == null)
{
inner = binder;
}
else
{
Assert.Same(inner, binder);
}
return Mock.Of<IModelBinder>();
}
return null;
});
var widgetIdProvider = new TestModelBinderProvider(c =>
{
Assert.Equal(typeof(WidgetId), c.Metadata.ModelType);
return Mock.Of<IModelBinder>();
});
options.Value.ModelBinderProviders.Add(widgetProvider);
options.Value.ModelBinderProviders.Add(widgetIdProvider);
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Widget)),
CacheToken = null, // We want the outermost provider to run twice.
};
// Act
var result1 = factory.CreateBinder(context);
var result2 = factory.CreateBinder(context);
// Assert
Assert.NotSame(result1, result2);
Assert.Equal(2, widgetProvider.SuccessCount);
Assert.Equal(1, widgetIdProvider.SuccessCount);
}
[Fact]
public void CreateBinder_Caches_NonRootNodes_WhenNonRootNodeReturnsNull()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = Options.Create(new MvcOptions());
IModelBinder inner = null;
var widgetProvider = new TestModelBinderProvider(c =>
{
if (c.Metadata.ModelType == typeof(Widget))
{
var binder = c.CreateBinder(c.Metadata.Properties[nameof(Widget.Id)]);
Assert.IsType<NoOpBinder>(binder);
if (inner == null)
{
inner = binder;
}
else
{
Assert.Same(inner, binder);
}
return Mock.Of<IModelBinder>();
}
return null;
});
var widgetIdProvider = new TestModelBinderProvider(c =>
{
Assert.Equal(typeof(WidgetId), c.Metadata.ModelType);
return null;
});
options.Value.ModelBinderProviders.Add(widgetProvider);
options.Value.ModelBinderProviders.Add(widgetIdProvider);
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Widget)),
CacheToken = null, // We want the outermost provider to run twice.
};
// Act
var result1 = factory.CreateBinder(context);
var result2 = factory.CreateBinder(context);
// Assert
Assert.NotSame(result1, result2);
Assert.Equal(2, widgetProvider.SuccessCount);
Assert.Equal(0, widgetIdProvider.SuccessCount);
}
// The fact that we use the ModelMetadata as the token is important for caching
// and sharing with TryUpdateModel.
[Fact]
public void CreateBinder_Caches_NonRootNodes_UsesModelMetadataAsToken()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = Options.Create(new MvcOptions());
IModelBinder inner = null;
var widgetProvider = new TestModelBinderProvider(c =>
{
if (c.Metadata.ModelType == typeof(Widget))
{
inner = c.CreateBinder(c.Metadata.Properties[nameof(Widget.Id)]);
return Mock.Of<IModelBinder>();
}
return null;
});
var widgetIdProvider = new TestModelBinderProvider(c =>
{
Assert.Equal(typeof(WidgetId), c.Metadata.ModelType);
return Mock.Of<IModelBinder>();
});
options.Value.ModelBinderProviders.Add(widgetProvider);
options.Value.ModelBinderProviders.Add(widgetIdProvider);
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Widget)),
CacheToken = null,
};
// Act 1
var result1 = factory.CreateBinder(context);
context.Metadata = context.Metadata.Properties[nameof(Widget.Id)];
context.CacheToken = context.Metadata;
// Act 2
var result2 = factory.CreateBinder(context);
// Assert
Assert.Same(inner, result2);
Assert.Equal(1, widgetProvider.SuccessCount);
Assert.Equal(1, widgetIdProvider.SuccessCount);
}
// This is a really weird case, but I wanted to make sure it's covered so it doesn't
// blow up in weird ways.
//
// If a binder provider tries to recursively create itself, but then returns null, we've
// already returned and possibly cached the PlaceholderBinder instance, we want to make sure that
// instance won't nullref.
[Fact]
public void CreateBinder_Caches_NonRootNodes_FixesUpPlaceholderBinder()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = Options.Create(new MvcOptions());
IModelBinder inner = null;
IModelBinder innerInner = null;
var widgetProvider = new TestModelBinderProvider(c =>
{
if (c.Metadata.ModelType == typeof(Widget))
{
inner = c.CreateBinder(c.Metadata.Properties[nameof(Widget.Id)]);
return Mock.Of<IModelBinder>();
}
return null;
});
var widgetIdProvider = new TestModelBinderProvider(c =>
{
Assert.Equal(typeof(WidgetId), c.Metadata.ModelType);
innerInner = c.CreateBinder(c.Metadata);
return null;
});
options.Value.ModelBinderProviders.Add(widgetProvider);
options.Value.ModelBinderProviders.Add(widgetIdProvider);
var factory = new ModelBinderFactory(
metadataProvider,
options,
GetServices());
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(Widget)),
CacheToken = null,
};
// Act 1
var result1 = factory.CreateBinder(context);
context.Metadata = context.Metadata.Properties[nameof(Widget.Id)];
context.CacheToken = context.Metadata;
// Act 2
var result2 = factory.CreateBinder(context);
// Assert
Assert.Same(inner, result2);
Assert.NotSame(inner, innerInner);
var placeholder = Assert.IsType<PlaceholderBinder>(innerInner);
Assert.IsType<NoOpBinder>(placeholder.Inner);
Assert.Equal(1, widgetProvider.SuccessCount);
Assert.Equal(0, widgetIdProvider.SuccessCount);
}
private IServiceProvider GetServices()
{
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
return services.BuildServiceProvider();
}
private class Widget
{
public WidgetId Id { get; set; }
}
private class WidgetBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
throw new NotImplementedException();
}
}
private class WidgetId
{
}
private class WidgetIdBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
throw new NotImplementedException();
}
}
private class Employee
{
public Employee Manager { get; set; }
}
private class TestModelBinderProvider : IModelBinderProvider
{
private readonly Func<ModelBinderProviderContext, IModelBinder> _factory;
public TestModelBinderProvider(Func<ModelBinderProviderContext, IModelBinder> factory)
{
_factory = factory;
}
public int SuccessCount { get; private set; }
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
var binder = _factory(context);
if (binder != null)
{
SuccessCount++;
}
return binder;
}
}
}
|