Skip to content

Commit 3ddcd4d

Browse files
committed
Merge in 'release/8.0-rc1' changes
2 parents d62ccad + 81fe483 commit 3ddcd4d

File tree

9 files changed

+290
-1
lines changed

9 files changed

+290
-1
lines changed

src/Mvc/Mvc.Abstractions/src/ModelBinding/BindingInfo.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using System.Reflection;
66
using Microsoft.AspNetCore.Mvc.Abstractions;
7+
using Microsoft.Extensions.DependencyInjection;
78

89
namespace Microsoft.AspNetCore.Mvc.ModelBinding;
910

@@ -35,6 +36,7 @@ public BindingInfo(BindingInfo other)
3536
PropertyFilterProvider = other.PropertyFilterProvider;
3637
RequestPredicate = other.RequestPredicate;
3738
EmptyBodyBehavior = other.EmptyBodyBehavior;
39+
ServiceKey = other.ServiceKey;
3840
}
3941

4042
/// <summary>
@@ -89,6 +91,11 @@ public Type? BinderType
8991
/// </summary>
9092
public EmptyBodyBehavior EmptyBodyBehavior { get; set; }
9193

94+
/// <summary>
95+
/// Get or sets the value used as the key when looking for a keyed service
96+
/// </summary>
97+
public object? ServiceKey { get; set; }
98+
9299
/// <summary>
93100
/// Constructs a new instance of <see cref="BindingInfo"/> from the given <paramref name="attributes"/>.
94101
/// <para>
@@ -169,6 +176,19 @@ public Type? BinderType
169176
break;
170177
}
171178

179+
// Keyed services
180+
if (attributes.OfType<FromKeyedServicesAttribute>().FirstOrDefault() is { } fromKeyedServicesAttribute)
181+
{
182+
if (bindingInfo.BindingSource != null)
183+
{
184+
throw new NotSupportedException(
185+
$"The {nameof(FromKeyedServicesAttribute)} is not supported on parameters that are also annotated with {nameof(IBindingSourceMetadata)}.");
186+
}
187+
isBindingInfoPresent = true;
188+
bindingInfo.BindingSource = BindingSource.Services;
189+
bindingInfo.ServiceKey = fromKeyedServicesAttribute.Key;
190+
}
191+
172192
return isBindingInfoPresent ? bindingInfo : null;
173193
}
174194

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Mvc.ModelBinding.BindingInfo.ServiceKey.get -> object?
3+
Microsoft.AspNetCore.Mvc.ModelBinding.BindingInfo.ServiceKey.set -> void

src/Mvc/Mvc.Abstractions/test/ModelBinding/BindingInfoTest.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
5+
using Microsoft.Extensions.DependencyInjection;
56
using Moq;
67

78
namespace Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -286,4 +287,43 @@ public void GetBindingInfo_WithAttributesAndModelMetadata_PreserveEmptyBodyDefau
286287
Assert.NotNull(bindingInfo);
287288
Assert.Equal(EmptyBodyBehavior.Default, bindingInfo.EmptyBodyBehavior);
288289
}
290+
291+
[Fact]
292+
public void GetBindingInfo_WithFromKeyedServicesAttribute()
293+
{
294+
// Arrange
295+
var key = new object();
296+
var attributes = new object[]
297+
{
298+
new FromKeyedServicesAttribute(key),
299+
};
300+
var modelType = typeof(Guid);
301+
var provider = new TestModelMetadataProvider();
302+
var modelMetadata = provider.GetMetadataForType(modelType);
303+
304+
// Act
305+
var bindingInfo = BindingInfo.GetBindingInfo(attributes, modelMetadata);
306+
307+
// Assert
308+
Assert.NotNull(bindingInfo);
309+
Assert.Same(BindingSource.Services, bindingInfo.BindingSource);
310+
Assert.Same(key, bindingInfo.ServiceKey);
311+
}
312+
313+
[Fact]
314+
public void GetBindingInfo_ThrowsWhenWithFromKeyedServicesAttributeAndIFromServiceMetadata()
315+
{
316+
// Arrange
317+
var attributes = new object[]
318+
{
319+
new FromKeyedServicesAttribute(new object()),
320+
new FromServicesAttribute()
321+
};
322+
var modelType = typeof(Guid);
323+
var provider = new TestModelMetadataProvider();
324+
var modelMetadata = provider.GetMetadataForType(modelType);
325+
326+
// Act and Assert
327+
Assert.Throws<NotSupportedException>(() => BindingInfo.GetBindingInfo(attributes, modelMetadata));
328+
}
289329
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#nullable enable
5+
6+
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
10+
11+
internal class KeyedServicesModelBinder : IModelBinder
12+
{
13+
private readonly object _key;
14+
private readonly bool _isOptional;
15+
16+
public KeyedServicesModelBinder(object key, bool isOptional)
17+
{
18+
_key = key ?? throw new ArgumentNullException(nameof(key));
19+
_isOptional = isOptional;
20+
}
21+
22+
public Task BindModelAsync(ModelBindingContext bindingContext)
23+
{
24+
var keyedServices = bindingContext.HttpContext.RequestServices as IKeyedServiceProvider;
25+
if (keyedServices == null)
26+
{
27+
bindingContext.Result = ModelBindingResult.Failed();
28+
return Task.CompletedTask;
29+
}
30+
31+
var model = _isOptional ?
32+
keyedServices.GetKeyedService(bindingContext.ModelType, _key) :
33+
keyedServices.GetRequiredKeyedService(bindingContext.ModelType, _key);
34+
35+
if (model != null)
36+
{
37+
bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
38+
}
39+
40+
bindingContext.Result = ModelBindingResult.Success(model);
41+
return Task.CompletedTask;
42+
}
43+
}

src/Mvc/Mvc.Core/src/ModelBinding/Binders/ServicesModelBinderProvider.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,17 @@ public class ServicesModelBinderProvider : IModelBinderProvider
2525
{
2626
// IsRequired will be false for a Reference Type
2727
// without a default value in a oblivious nullability context
28-
// however, for services we shoud treat them as required
28+
// however, for services we should treat them as required
2929
var isRequired = context.Metadata.IsRequired ||
3030
(context.Metadata.Identity.ParameterInfo?.HasDefaultValue != true &&
3131
!context.Metadata.ModelType.IsValueType &&
3232
context.Metadata.NullabilityState == NullabilityState.Unknown);
3333

34+
if (context.BindingInfo.ServiceKey != null)
35+
{
36+
return new KeyedServicesModelBinder(context.BindingInfo.ServiceKey, !isRequired);
37+
}
38+
3439
return isRequired ? _servicesBinder : _optionalServicesBinder;
3540
}
3641

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Net.Http;
5+
6+
namespace Microsoft.AspNetCore.Mvc.FunctionalTests;
7+
8+
public class KeyedServicesTests : IClassFixture<MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting>>
9+
{
10+
public KeyedServicesTests(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
11+
{
12+
Client = fixture.CreateDefaultClient();
13+
}
14+
15+
public HttpClient Client { get; }
16+
17+
[Fact]
18+
public async Task ExplicitSingleFromKeyedServiceAttribute()
19+
{
20+
// Arrange
21+
var okRequest = new HttpRequestMessage(HttpMethod.Get, "/services/GetOk");
22+
var notokRequest = new HttpRequestMessage(HttpMethod.Get, "/services/GetNotOk");
23+
24+
// Act
25+
var okResponse = await Client.SendAsync(okRequest);
26+
var notokResponse = await Client.SendAsync(notokRequest);
27+
28+
// Assert
29+
Assert.True(okResponse.IsSuccessStatusCode);
30+
Assert.True(notokResponse.IsSuccessStatusCode);
31+
Assert.Equal("OK", await okResponse.Content.ReadAsStringAsync());
32+
Assert.Equal("NOT OK", await notokResponse.Content.ReadAsStringAsync());
33+
}
34+
35+
[Fact]
36+
public async Task ExplicitMultipleFromKeyedServiceAttribute()
37+
{
38+
// Arrange
39+
var request = new HttpRequestMessage(HttpMethod.Get, "/services/GetBoth");
40+
41+
// Act
42+
var response = await Client.SendAsync(request);
43+
44+
// Assert
45+
Assert.True(response.IsSuccessStatusCode);
46+
Assert.Equal("OK,NOT OK", await response.Content.ReadAsStringAsync());
47+
}
48+
49+
[Fact]
50+
public async Task ExplicitSingleFromKeyedServiceAttributeWithNullKey()
51+
{
52+
// Arrange
53+
var request = new HttpRequestMessage(HttpMethod.Get, "/services/GetKeyNull");
54+
55+
// Act
56+
var response = await Client.SendAsync(request);
57+
58+
// Assert
59+
Assert.True(response.IsSuccessStatusCode);
60+
Assert.Equal("DEFAULT", await response.Content.ReadAsStringAsync());
61+
}
62+
63+
[Fact]
64+
public async Task ExplicitSingleFromKeyedServiceAttributeOptionalNotRegistered()
65+
{
66+
// Arrange
67+
var request = new HttpRequestMessage(HttpMethod.Get, "/services/GetOptionalNotRegistered");
68+
69+
// Act
70+
var response = await Client.SendAsync(request);
71+
72+
// Assert
73+
Assert.True(response.IsSuccessStatusCode);
74+
Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
75+
}
76+
77+
[Fact]
78+
public async Task ExplicitSingleFromKeyedServiceAttributeRequiredNotRegistered()
79+
{
80+
// Arrange
81+
var request = new HttpRequestMessage(HttpMethod.Get, "/services/GetRequiredNotRegistered");
82+
83+
// Act
84+
var response = await Client.SendAsync(request);
85+
86+
// Assert
87+
Assert.False(response.IsSuccessStatusCode);
88+
}
89+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Mvc;
5+
6+
namespace BasicWebSite;
7+
8+
[ApiController]
9+
[Route("/services")]
10+
public class CustomServicesApiController : Controller
11+
{
12+
[HttpGet("GetOk")]
13+
public ActionResult<string> GetOk([FromKeyedServices("ok_service")] ICustomService service)
14+
{
15+
return service.Process();
16+
}
17+
18+
[HttpGet("GetNotOk")]
19+
public ActionResult<string> GetNotOk([FromKeyedServices("not_ok_service")] ICustomService service)
20+
{
21+
return service.Process();
22+
}
23+
24+
[HttpGet("GetBoth")]
25+
public ActionResult<string> GetBoth(
26+
[FromKeyedServices("ok_service")] ICustomService s1,
27+
[FromKeyedServices("not_ok_service")] ICustomService s2)
28+
{
29+
return $"{s1.Process()},{s2.Process()}";
30+
}
31+
32+
[HttpGet("GetKeyNull")]
33+
public ActionResult<string> GetKeyNull([FromKeyedServices(null)] ICustomService service)
34+
{
35+
return service.Process();
36+
}
37+
38+
# nullable enable
39+
40+
[HttpGet("GetOptionalNotRegistered")]
41+
public ActionResult<string> GetOptionalNotRegistered([FromKeyedServices("no_existing_key")] ICustomService? service)
42+
{
43+
if (service != null)
44+
{
45+
throw new Exception("Service should not have been resolved");
46+
}
47+
return string.Empty;
48+
}
49+
50+
[HttpGet("GetRequiredNotRegistered")]
51+
public ActionResult<string> GetRequiredNotRegistered([FromKeyedServices("no_existing_key")] ICustomService service)
52+
{
53+
return service.Process();
54+
}
55+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using BasicWebSite.Models;
5+
using Microsoft.AspNetCore.Http.HttpResults;
6+
using Microsoft.AspNetCore.Mvc;
7+
8+
namespace BasicWebSite;
9+
10+
public interface ICustomService
11+
{
12+
string Process();
13+
}
14+
15+
public class OkCustomService : ICustomService
16+
{
17+
public string Process() => "OK";
18+
public override string ToString() => Process();
19+
}
20+
21+
public class BadCustomService : ICustomService
22+
{
23+
public string Process() => "NOT OK";
24+
public override string ToString() => Process();
25+
}
26+
27+
public class DefaultCustomService : ICustomService
28+
{
29+
public string Process() => "DEFAULT";
30+
public override string ToString() => Process();
31+
public static DefaultCustomService Instance => new DefaultCustomService();
32+
}

src/Mvc/test/WebSites/BasicWebSite/StartupWithoutEndpointRouting.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public void ConfigureServices(IServiceCollection services)
4343
services.AddSingleton<IActionDescriptorProvider, ActionDescriptorCreationCounter>();
4444
services.AddHttpContextAccessor();
4545
services.AddSingleton<ContactsRepository>();
46+
services.AddKeyedSingleton<ICustomService, OkCustomService>("ok_service");
47+
services.AddKeyedSingleton<ICustomService, BadCustomService>("not_ok_service");
48+
services.AddSingleton<ICustomService, DefaultCustomService>();
4649
services.AddScoped<RequestIdService>();
4750
services.AddTransient<ServiceActionFilter>();
4851
services.AddScoped<TestResponseGenerator>();

0 commit comments

Comments
 (0)