From d2aff42e25dfb60684563f147ea82f8a8ec75b9c Mon Sep 17 00:00:00 2001 From: Harsh Gupta Date: Mon, 17 Nov 2014 20:09:14 -0800 Subject: [PATCH] Adding FromServicesAttribute. --- .../ModelBinders/ServicesModelBinder.cs | 27 +++++++++++ .../BinderMetadata/FromSericesAttribute.cs | 17 +++++++ .../IServiceActivatorBinderMetadata.cs | 12 +++++ .../Binders/MetadataAwareBinder.cs | 3 +- src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs | 1 + .../ModelBindingTests.cs | 47 ++++++++++++++++++- .../RequestServicesTest.cs | 2 + .../MvcOptionSetupTest.cs | 20 ++++---- .../ModelBindingWebSite/CaculatorContext.cs | 19 ++++++++ .../Controllers/FromAttributesController.cs | 1 + .../FromServices_CalculatorController.cs | 25 ++++++++++ .../ModelBindingWebSite/DefaultCalculator.cs | 32 +++++++++++++ .../DefaultCalculatorContext.cs | 12 +++++ .../ModelBindingWebSite/ICalculator.cs | 10 ++++ test/WebSites/ModelBindingWebSite/Startup.cs | 3 ++ .../Controllers/OtherController.cs | 12 +++++ .../Models/RequestModel.cs | 13 +++++ 17 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/ModelBinders/ServicesModelBinder.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromSericesAttribute.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IServiceActivatorBinderMetadata.cs create mode 100644 test/WebSites/ModelBindingWebSite/CaculatorContext.cs create mode 100644 test/WebSites/ModelBindingWebSite/Controllers/FromServices_CalculatorController.cs create mode 100644 test/WebSites/ModelBindingWebSite/DefaultCalculator.cs create mode 100644 test/WebSites/ModelBindingWebSite/DefaultCalculatorContext.cs create mode 100644 test/WebSites/ModelBindingWebSite/ICalculator.cs create mode 100644 test/WebSites/RequestServicesWebSite/Models/RequestModel.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinders/ServicesModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinders/ServicesModelBinder.cs new file mode 100644 index 0000000000..d12a81c8ef --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinders/ServicesModelBinder.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// An which understands + /// and activates a given model using . + /// + public class ServicesModelBinder : MetadataAwareBinder + { + /// + protected override Task BindAsync( + [NotNull] ModelBindingContext bindingContext, + [NotNull] IServiceActivatorBinderMetadata metadata) + { + var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices; + bindingContext.Model = requestServices.GetRequiredService(bindingContext.ModelType); + return Task.FromResult(true); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromSericesAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromSericesAttribute.cs new file mode 100644 index 0000000000..ba84b2fe81 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromSericesAttribute.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Mvc.ModelBinding; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// This attribute is used on action parameters or model properties to indicate that + /// they will be bound using service provider. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class FromServicesAttribute : Attribute, IServiceActivatorBinderMetadata + { + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IServiceActivatorBinderMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IServiceActivatorBinderMetadata.cs new file mode 100644 index 0000000000..5b18b12fc2 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IServiceActivatorBinderMetadata.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// Metadata interface that indicates model binding should use the service container to get the value for a model. + /// + public interface IServiceActivatorBinderMetadata : IBinderMetadata + { + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MetadataAwareBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MetadataAwareBinder.cs index 75531e3245..0bf3c4cfbe 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MetadataAwareBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MetadataAwareBinder.cs @@ -19,7 +19,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// The binding context which has the object to be bound. /// The associated with the current binder. /// A Task with a bool implying the success or failure of the operation. - protected abstract Task BindAsync(ModelBindingContext bindingContext, TBinderMetadata metadata); + protected abstract Task BindAsync([NotNull] ModelBindingContext bindingContext, + [NotNull] TBinderMetadata metadata); public Task BindModelAsync(ModelBindingContext context) { diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index 2c0e2f4690..9f9e0da1ee 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -27,6 +27,7 @@ namespace Microsoft.AspNet.Mvc options.ViewEngines.Add(typeof(RazorViewEngine)); // Set up ModelBinding + options.ModelBinders.Add(typeof(ServicesModelBinder)); options.ModelBinders.Add(typeof(BodyModelBinder)); options.ModelBinders.Add(new TypeConverterModelBinder()); options.ModelBinders.Add(new TypeMatchModelBinder()); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs index 5ec9b55de5..422572019b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs @@ -63,7 +63,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Act var response = await client.PostAsync("http://localhost/Home/GetCustomer?Id=1234", content); - + //Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var customer = JsonConvert.DeserializeObject( @@ -75,6 +75,51 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("dummy", customer.Name); } + [Fact] + public async Task CanModelBindServiceToAnArgument() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/FromServices_Calculator/Add?left=1234&right=1"); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("1235", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task CanModelBindServiceToAProperty() + { + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync( + "http://localhost/FromServices_Calculator/Calculate?Left=10&Right=5&Operator=*"); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("50", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task CanModelBindServiceToAProperty_OnBaseType() + { + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync( + "http://localhost/FromServices_Calculator/CalculateWithPrecision?Left=10&Right=5&Operator=*"); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("50", await response.Content.ReadAsStringAsync()); + } + [Fact] public async Task MultipleParametersMarkedWithFromBody_Throws() { diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RequestServicesTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RequestServicesTest.cs index 851b7cb85e..8bf7da227e 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/RequestServicesTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RequestServicesTest.cs @@ -23,6 +23,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests [InlineData("http://localhost/Other/FromFilter")] [InlineData("http://localhost/Other/FromView")] [InlineData("http://localhost/Other/FromViewComponent")] + [InlineData("http://localhost/Other/FromModelProperty")] + [InlineData("http://localhost/Other/FromActionArgument")] public async Task RequestServices(string url) { // Arrange diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs index 714ecdcf17..f6cdc38906 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs @@ -38,15 +38,17 @@ namespace Microsoft.AspNet.Mvc setup.Configure(mvcOptions); // Assert - Assert.Equal(8, mvcOptions.ModelBinders.Count); - Assert.Equal(typeof(BodyModelBinder), mvcOptions.ModelBinders[0].OptionType); - Assert.Equal(typeof(TypeConverterModelBinder), mvcOptions.ModelBinders[1].OptionType); - Assert.Equal(typeof(TypeMatchModelBinder), mvcOptions.ModelBinders[2].OptionType); - Assert.Equal(typeof(CancellationTokenModelBinder), mvcOptions.ModelBinders[3].OptionType); - Assert.Equal(typeof(ByteArrayModelBinder), mvcOptions.ModelBinders[4].OptionType); - Assert.Equal(typeof(GenericModelBinder), mvcOptions.ModelBinders[5].OptionType); - Assert.Equal(typeof(MutableObjectModelBinder), mvcOptions.ModelBinders[6].OptionType); - Assert.Equal(typeof(ComplexModelDtoModelBinder), mvcOptions.ModelBinders[7].OptionType); + var i = 0; + Assert.Equal(9, mvcOptions.ModelBinders.Count); + Assert.Equal(typeof(ServicesModelBinder), mvcOptions.ModelBinders[i++].OptionType); + Assert.Equal(typeof(BodyModelBinder), mvcOptions.ModelBinders[i++].OptionType); + Assert.Equal(typeof(TypeConverterModelBinder), mvcOptions.ModelBinders[i++].OptionType); + Assert.Equal(typeof(TypeMatchModelBinder), mvcOptions.ModelBinders[i++].OptionType); + Assert.Equal(typeof(CancellationTokenModelBinder), mvcOptions.ModelBinders[i++].OptionType); + Assert.Equal(typeof(ByteArrayModelBinder), mvcOptions.ModelBinders[i++].OptionType); + Assert.Equal(typeof(GenericModelBinder), mvcOptions.ModelBinders[i++].OptionType); + Assert.Equal(typeof(MutableObjectModelBinder), mvcOptions.ModelBinders[i++].OptionType); + Assert.Equal(typeof(ComplexModelDtoModelBinder), mvcOptions.ModelBinders[i++].OptionType); } [Fact] diff --git a/test/WebSites/ModelBindingWebSite/CaculatorContext.cs b/test/WebSites/ModelBindingWebSite/CaculatorContext.cs new file mode 100644 index 0000000000..e7eab38aa0 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/CaculatorContext.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace ModelBindingWebSite +{ + public class CalculatorContext + { + [FromServices] + public ICalculator Calculator { get; set; } + + public int Left { get; set; } + + public int Right { get; set; } + + public char Operator { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FromAttributesController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FromAttributesController.cs index 3d67be77be..85d9d060a5 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/FromAttributesController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/FromAttributesController.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding; namespace ModelBindingWebSite.Controllers { diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FromServices_CalculatorController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FromServices_CalculatorController.cs new file mode 100644 index 0000000000..e2b1cbeecc --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Controllers/FromServices_CalculatorController.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace ModelBindingWebSite.Controllers +{ + public class FromServices_CalculatorController : Controller + { + public int Calculate(CalculatorContext context) + { + return context.Calculator.Operation(context.Operator, context.Left, context.Right); + } + + public int Add(int left, int right, [FromServices] ICalculator calculator) + { + return calculator.Operation('+', left, right); + } + + public int CalculateWithPrecision(DefaultCalculatorContext context) + { + return context.Calculator.Operation(context.Operator, context.Left, context.Right); + } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/DefaultCalculator.cs b/test/WebSites/ModelBindingWebSite/DefaultCalculator.cs new file mode 100644 index 0000000000..61cd6f9a89 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/DefaultCalculator.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Framework.Logging; + +namespace ModelBindingWebSite +{ + public class DefaultCalculator : ICalculator + { + private ILogger _logger; + + public DefaultCalculator(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.Create(typeof(DefaultCalculator).FullName); + } + + public int Operation(char @operator, int left, int right) + { + switch (@operator) + { + case '+': return left + right; + case '-': return left - right; + case '*': return left * right; + case '/': return left / right; + default: + _logger.WriteError("Unrecognized operator:" + @operator); + throw new InvalidOperationException(); + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/DefaultCalculatorContext.cs b/test/WebSites/ModelBindingWebSite/DefaultCalculatorContext.cs new file mode 100644 index 0000000000..bf8a9c6114 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/DefaultCalculatorContext.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace ModelBindingWebSite +{ + public class DefaultCalculatorContext : CalculatorContext + { + public int Precision { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/ICalculator.cs b/test/WebSites/ModelBindingWebSite/ICalculator.cs new file mode 100644 index 0000000000..40c1952625 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/ICalculator.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace ModelBindingWebSite +{ + public interface ICalculator + { + int Operation(char @operator, int left, int right); + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Startup.cs b/test/WebSites/ModelBindingWebSite/Startup.cs index 4649eb800b..0ea1b6182b 100644 --- a/test/WebSites/ModelBindingWebSite/Startup.cs +++ b/test/WebSites/ModelBindingWebSite/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Routing; using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; namespace ModelBindingWebSite { @@ -24,6 +25,8 @@ namespace ModelBindingWebSite m.MaxModelValidationErrors = 8; m.ModelBinders.Insert(0, typeof(TestMetadataAwareBinder)); }); + + services.AddSingleton(); }); // Add MVC to the request pipeline diff --git a/test/WebSites/RequestServicesWebSite/Controllers/OtherController.cs b/test/WebSites/RequestServicesWebSite/Controllers/OtherController.cs index e48080c0e5..181d6cac50 100644 --- a/test/WebSites/RequestServicesWebSite/Controllers/OtherController.cs +++ b/test/WebSites/RequestServicesWebSite/Controllers/OtherController.cs @@ -39,5 +39,17 @@ namespace RequestServicesWebSite { return View("ViewComponent"); } + + [HttpGet] + public string FromModelProperty(RequestModel requestContext) + { + return requestContext.RequestIdService.RequestId; + } + + [HttpGet] + public string FromActionArgument([FromServices] RequestIdService requestIdService) + { + return requestIdService.RequestId; + } } } \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/Models/RequestModel.cs b/test/WebSites/RequestServicesWebSite/Models/RequestModel.cs new file mode 100644 index 0000000000..29444be583 --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/Models/RequestModel.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace RequestServicesWebSite +{ + public class RequestModel + { + [FromServices] + public RequestIdService RequestIdService { get; set; } + } +} \ No newline at end of file