Skip to content
19 changes: 19 additions & 0 deletions src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
Expand Down Expand Up @@ -474,6 +475,15 @@ internal IReadOnlyDictionary<ModelMetadata, ModelMetadata> BoundConstructorPrope
/// </remarks>
public Type UnderlyingOrModelType { get; private set; } = default!;

/// <summary>
/// Gets a value indicating the NullabilityState of the value or reference type.
/// </summary>
/// <remarks>
/// The state will be set for Parameters and Properties <see cref="ModelMetadataKind"/>
/// otherwise the state will be <c>NullabilityState.Unknown</c>
/// </remarks>
internal NullabilityState NullabilityState { get; set; }

/// <summary>
/// Gets a property getter delegate to get the property value from a model object.
/// </summary>
Expand Down Expand Up @@ -614,6 +624,15 @@ private void InitializeTypeInformation()
var collectionType = ClosedGenericMatcher.ExtractGenericInterface(ModelType, typeof(ICollection<>));
IsCollectionType = collectionType != null;

var nullabilityContext = new NullabilityInfoContext();
var nullability = MetadataKind switch
{
ModelMetadataKind.Parameter => Identity.ParameterInfo != null ? nullabilityContext.Create(Identity.ParameterInfo!) : null,
ModelMetadataKind.Property => Identity.PropertyInfo != null ? nullabilityContext.Create(Identity.PropertyInfo!) : null,
_ => null
};
NullabilityState = nullability?.ReadState ?? NullabilityState.Unknown;

if (ModelType == typeof(string) || !typeof(IEnumerable).IsAssignableFrom(ModelType))
{
// Do nothing, not Enumerable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
/// </summary>
public class ServicesModelBinder : IModelBinder
{
internal bool IsOptional { get; set; }

/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
Expand All @@ -23,9 +25,14 @@ public Task BindModelAsync(ModelBindingContext bindingContext)
}

var requestServices = bindingContext.HttpContext.RequestServices;
var model = requestServices.GetRequiredService(bindingContext.ModelType);
var model = IsOptional ?
requestServices.GetService(bindingContext.ModelType) :
requestServices.GetRequiredService(bindingContext.ModelType);

bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
if (model != null)
{
bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
}
Comment thread
pranavkm marked this conversation as resolved.

bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@

#nullable enable

using System.Reflection;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;

/// <summary>
/// An <see cref="IModelBinderProvider"/> for binding from the <see cref="IServiceProvider"/>.
/// </summary>
public class ServicesModelBinderProvider : IModelBinderProvider
{
// ServicesModelBinder does not have any state. Re-use the same instance for binding.

private readonly ServicesModelBinder _modelBinder = new ServicesModelBinder();
Comment thread
brunolins16 marked this conversation as resolved.
private readonly ServicesModelBinder _optionalServicesBinder = new() { IsOptional = true };
private readonly ServicesModelBinder _servicesBinder = new();

/// <inheritdoc />
public IModelBinder? GetBinder(ModelBinderProviderContext context)
Expand All @@ -25,7 +26,15 @@ public class ServicesModelBinderProvider : IModelBinderProvider
if (context.BindingInfo.BindingSource != null &&
context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Services))
{
return _modelBinder;
// IsRequired will be false for a Reference Type
// without a default value in a oblivious nullability context
// however, for services we shoud treat them as required
var isRequired = context.Metadata.IsRequired ||
(context.Metadata.Identity.ParameterInfo?.HasDefaultValue != true &&
!context.Metadata.ModelType.IsValueType &&
context.Metadata.NullabilityState == NullabilityState.Unknown);

return isRequired ? _servicesBinder : _optionalServicesBinder;
}

return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;

public class ServicesModelBinderProviderTest
Expand Down Expand Up @@ -51,7 +53,66 @@ public void Create_WhenBindingSourceIsFromServices_ReturnsBinder()
Assert.IsType<ServicesModelBinder>(result);
}

[Theory]
[MemberData(nameof(ParameterInfoData))]
public void Create_WhenBindingSourceIsNullableFromServices_ReturnsBinder(ParameterInfo parameterInfo, bool isOptional)
{
// Arrange
var provider = new ServicesModelBinderProvider();

var context = new TestModelBinderProviderContext(parameterInfo);

// Act
var result = provider.GetBinder(context);

// Assert
var binder = Assert.IsType<ServicesModelBinder>(result);
Assert.Equal(isOptional, binder.IsOptional);
}

private class IPersonService
{
}

public static TheoryData<ParameterInfo, bool> ParameterInfoData()
{
return new TheoryData<ParameterInfo, bool>()
{
{ ParameterInfos.NullableParameterInfo, true },
{ ParameterInfos.DefaultValueParameterInfo, true },
{ ParameterInfos.NonNullabilityContextParameterInfo, false },
{ ParameterInfos.NonNullableParameterInfo, false },
};
}

private class ParameterInfos
{
#nullable disable
public void TestMethod([FromServices] IPersonService param1, [FromServices] IPersonService param2 = null)
{ }
#nullable restore

#nullable enable
public void TestMethod2([FromServices] IPersonService param1, [FromServices] IPersonService? param2)
{ }
#nullable restore

public static ParameterInfo NonNullableParameterInfo
= typeof(ParameterInfos)
.GetMethod(nameof(ParameterInfos.TestMethod2))
.GetParameters()[0];
public static ParameterInfo NullableParameterInfo
= typeof(ParameterInfos)
.GetMethod(nameof(ParameterInfos.TestMethod2))
.GetParameters()[1];

Comment thread
brunolins16 marked this conversation as resolved.
public static ParameterInfo NonNullabilityContextParameterInfo
= typeof(ParameterInfos)
.GetMethod(nameof(ParameterInfos.TestMethod))
.GetParameters()[0];
public static ParameterInfo DefaultValueParameterInfo
= typeof(ParameterInfos)
.GetMethod(nameof(ParameterInfos.TestMethod))
.GetParameters()[1];
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
Comment thread
brunolins16 marked this conversation as resolved.
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand Down Expand Up @@ -38,6 +39,21 @@ public TestModelBinderProviderContext(Type modelType, BindingInfo bindingInfo)
(Services, MvcOptions) = GetServicesAndOptions();
}

public TestModelBinderProviderContext(ParameterInfo parameterInfo)
{
Metadata = CachedMetadataProvider.GetMetadataForParameter(parameterInfo);
MetadataProvider = CachedMetadataProvider;
_bindingInfo = new BindingInfo
{
BinderModelName = Metadata.BinderModelName,
BinderType = Metadata.BinderType,
BindingSource = Metadata.BindingSource,
PropertyFilterProvider = Metadata.PropertyFilterProvider,
};

(Services, MvcOptions) = GetServicesAndOptions();
}

public override BindingInfo BindingInfo => _bindingInfo;

public override ModelMetadata Metadata { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;

Expand Down Expand Up @@ -180,6 +181,155 @@ public async Task BindParameterFromService_NoService_Throws()
Assert.Contains(typeof(IActionResult).FullName, exception.Message);
}

private class TestController
{
#nullable enable
public void Action(IActionResult? service, ITypeActivatorCache? service2)
{ }
#nullable restore

public void ActionWithDefaultValue(IActionResult service = default, ITypeActivatorCache service2 = default)
{ }
}

[Fact]
public async Task BindNullableParameterFromService_WithData_GetBounds()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameters = typeof(TestController).GetMethod(nameof(TestController.Action)).GetParameters();
var parameter = new ControllerParameterDescriptor
{
Name = "ControllerProperty",
BindingInfo = new BindingInfo
{
BindingSource = BindingSource.Services,
},
ParameterInfo = parameters[1],
// Use a service type already in defaults.
ParameterType = typeof(ITypeActivatorCache),
};

var testContext = ModelBindingTestHelper.GetTestContext();
var modelState = testContext.ModelState;

// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

// Model
var provider = Assert.IsAssignableFrom<ITypeActivatorCache>(modelBindingResult.Model);
Assert.NotNull(provider);

// ModelState
Assert.True(modelState.IsValid);
Assert.Empty(modelState.Keys);
}

[Fact]
public async Task BindNullableParameterFromService_NoService_BindsToNull()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameters = typeof(TestController).GetMethod(nameof(TestController.Action)).GetParameters();
var parameter = new ControllerParameterDescriptor
{
Name = "ControllerProperty",
BindingInfo = new BindingInfo
{
BindingSource = BindingSource.Services,
},
ParameterInfo = parameters[0],
// Use a service type not available in DI.
ParameterType = typeof(IActionResult),
};

var testContext = ModelBindingTestHelper.GetTestContext();
var modelState = testContext.ModelState;

// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

// Assert
// ModelBindingResult
Assert.True(modelBindingResult.IsModelSet);

// Model
Assert.Null(modelBindingResult.Model);

// ModelState
Assert.True(modelState.IsValid);
Assert.Empty(modelState);
}

[Fact]
public async Task BindParameterWithDefaultValueFromService_WithData_GetBounds()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameters = typeof(TestController).GetMethod(nameof(TestController.ActionWithDefaultValue)).GetParameters();
var parameter = new ControllerParameterDescriptor
{
Name = "ControllerProperty",
BindingInfo = new BindingInfo
{
BindingSource = BindingSource.Services,
},
ParameterInfo = parameters[1],
// Use a service type already in defaults.
ParameterType = typeof(ITypeActivatorCache),
};

var testContext = ModelBindingTestHelper.GetTestContext();
var modelState = testContext.ModelState;

// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

// Model
var provider = Assert.IsAssignableFrom<ITypeActivatorCache>(modelBindingResult.Model);
Assert.NotNull(provider);

// ModelState
Assert.True(modelState.IsValid);
Assert.Empty(modelState.Keys);
}

[Fact]
public async Task BindParameterWithDefaultValueFromService_NoService_BindsToDefaultValue()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameters = typeof(TestController).GetMethod(nameof(TestController.ActionWithDefaultValue)).GetParameters();
var parameter = new ControllerParameterDescriptor
{
Name = "ControllerProperty",
BindingInfo = new BindingInfo
{
BindingSource = BindingSource.Services,
},
ParameterInfo = parameters[0],
// Use a service type not available in DI.
ParameterType = typeof(IActionResult),
};

var testContext = ModelBindingTestHelper.GetTestContext();
var modelState = testContext.ModelState;

// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

// Assert
// ModelBindingResult
Assert.True(modelBindingResult.IsModelSet);

// Model
Assert.Null(modelBindingResult.Model);

// ModelState
Assert.True(modelState.IsValid);
Assert.Empty(modelState);
}

private class Person
{
public ITypeActivatorCache Service { get; set; }
Expand Down