Add `ModelMetadata.AdditionalValues` property bag

- #1758
- provide the property bag in `ModelMetadata`; seal it in `CachedModelMetadata`
- add unit tests
- include use of `AdditionalValues` in model binding functional test

nits:
- expose `AdditionalValues` as an `IDictionary` though MVC 5 uses `Dictionary`
- seal `ModelMetadata.Properties` collection as well
- cover a few properties previously missed in `CachedDataAnnotationsModelMetadataTest`
- correct two `ModelMetadataTest` method names
This commit is contained in:
Doug Bunting 2015-02-05 11:24:42 -08:00
parent b3c38bc573
commit 3303286288
10 changed files with 275 additions and 7 deletions

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
@ -82,6 +83,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
PrototypeCache = prototypeCache;
}
// Sealing for consistency with other properties.
// ModelMetadata already caches the collection and we have no use case for a ComputeAdditionalValues() method.
/// <inheritdoc />
public sealed override IDictionary<string, object> AdditionalValues
{
get
{
return base.AdditionalValues;
}
}
/// <inheritdoc />
public sealed override IBinderMetadata BinderMetadata
{
@ -419,6 +431,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
// Sealing for consistency with other properties.
// ModelMetadata already caches the collection and we have no use case for a ComputeProperties() method.
/// <inheritdoc />
public override ModelPropertyCollection Properties
{
get
{
return base.Properties;
}
}
/// <inheritdoc />
public sealed override bool ShowForDisplay
{

View File

@ -47,6 +47,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
_isRequired = !modelType.AllowsNullValue();
}
/// <summary>
/// Gets a collection of additional information about the model.
/// </summary>
public virtual IDictionary<string, object> AdditionalValues { get; }
= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets or sets the name of a model if specified explicitly using <see cref="IModelNameProvider"/>.
/// </summary>

View File

@ -1551,5 +1551,55 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var result = await response.Content.ReadAsStringAsync();
Assert.Equal("The value 'random string' is not valid for birthdate.", result);
}
[Fact]
public async Task OverriddenMetadataProvider_CanChangeAdditionalValues()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var url = "http://localhost/AdditionalValues";
var expectedDictionary = new Dictionary<string, string>
{
{ "key1", "7d6d0de2-8d59-49ac-99cc-881423b75a76" },
{ "key2", "value2" },
};
// Act
var response = await client.GetAsync(url);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync();
var dictionary = JsonConvert.DeserializeObject<IDictionary<string, string>>(responseContent);
Assert.Equal(expectedDictionary, dictionary);
}
[Fact]
public async Task OverriddenMetadataProvider_CanUseAttributesToChangeAdditionalValues()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var url = "http://localhost/GroupNames";
var expectedDictionary = new Dictionary<string, string>
{
{ "Model", "MakeAndModelGroup" },
{ "Make", "MakeAndModelGroup" },
{ "Vin", null },
{ "Year", null },
{ "InspectedDates", null },
{ "LastUpdatedTrackingId", "TrackingIdGroup" },
};
// Act
var response = await client.GetAsync(url);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync();
var dictionary = JsonConvert.DeserializeObject<IDictionary<string, string>>(responseContent);
Assert.Equal(expectedDictionary, dictionary);
}
}
}

View File

@ -29,11 +29,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
attributes: attributes);
// Assert
Assert.NotNull(metadata.AdditionalValues);
Assert.Empty(metadata.AdditionalValues);
Assert.Null(metadata.Container);
Assert.Null(metadata.ContainerType);
Assert.True(metadata.ConvertEmptyStringToNull);
Assert.False(metadata.HasNonDefaultEditFormat);
Assert.False(metadata.HideSurroundingHtml);
Assert.True(metadata.HtmlEncode);
Assert.False(metadata.IsCollectionType);
Assert.True(metadata.IsComplexType);
Assert.False(metadata.IsNullableValueType);
Assert.False(metadata.IsReadOnly);
Assert.False(metadata.IsRequired);
Assert.True(metadata.ShowForDisplay);
@ -48,6 +55,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Null(metadata.SimpleDisplayText);
Assert.Null(metadata.TemplateHint);
Assert.Null(metadata.Model);
Assert.Equal(typeof(object), metadata.ModelType);
Assert.Equal(typeof(object), metadata.RealModelType);
Assert.Null(metadata.PropertyName);
Assert.Equal(ModelMetadata.DefaultOrder, metadata.Order);
Assert.Null(metadata.BinderModelName);

View File

@ -5,7 +5,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Reflection;
using Xunit;
@ -76,6 +75,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new ModelMetadata(provider, typeof(Exception), () => "model", typeof(string), "propertyName");
// Assert
Assert.NotNull(metadata.AdditionalValues);
Assert.Empty(metadata.AdditionalValues);
Assert.Equal(typeof(Exception), metadata.ContainerType);
Assert.Null(metadata.Container);
@ -114,6 +115,58 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Null(metadata.PropertyBindingPredicateProvider);
}
// AdditionalValues
[Fact]
public void AdditionalValues_CreatedOnce()
{
// Arrange
var provider = new EmptyModelMetadataProvider();
var metadata = new ModelMetadata(
provider,
containerType: null,
modelAccessor: () => null,
modelType: typeof(object),
propertyName: null);
// Act
var result1 = metadata.AdditionalValues;
var result2 = metadata.AdditionalValues;
// Assert
Assert.Same(result1, result2);
}
[Fact]
public void AdditionalValues_ChangesPersist()
{
// Arrange
var provider = new EmptyModelMetadataProvider();
var metadata = new ModelMetadata(
provider,
containerType: null,
modelAccessor: () => null,
modelType: typeof(object),
propertyName: null);
var valuesDictionary = new Dictionary<string, object>
{
{ "key1", new object() },
{ "key2", "value2" },
{ "key3", new object() },
};
// Act
foreach (var keyValuePair in valuesDictionary)
{
metadata.AdditionalValues.Add(keyValuePair);
}
// Assert
Assert.Equal(valuesDictionary, metadata.AdditionalValues);
}
// IsComplexType
private struct IsComplexTypeModel
@ -304,7 +357,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
get
{
// ModelMetadata does not reorder properties Reflection returns without an Order override.
// ModelMetadata does not reorder properties the provider returns without an Order override.
return new TheoryData<IEnumerable<string>, IEnumerable<string>>
{
{
@ -329,7 +382,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
[Theory]
[MemberData(nameof(PropertyNamesTheoryData))]
public void PropertiesProperty_WithDefaultOrder_OrdersPropertyNamesAlphabetically(
public void PropertiesProperty_WithDefaultOrder_OrdersPropertyNamesAsProvided(
IEnumerable<string> originalNames,
IEnumerable<string> expectedNames)
{
@ -418,7 +471,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
[Theory]
[MemberData(nameof(PropertyNamesAndOrdersTheoryData))]
public void PropertiesProperty_OrdersPropertyNamesUsingOrder_ThenAlphabetically(
public void PropertiesProperty_OrdersPropertyNamesUsingOrder_ThenAsProvided(
IEnumerable<KeyValuePair<string, int>> originalNamesAndOrders,
IEnumerable<string> expectedNames)
{

View File

@ -0,0 +1,54 @@
// 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.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNet.Mvc.ModelBinding;
using ModelBindingWebSite.Models;
namespace ModelBindingWebSite
{
public class AdditionalValuesMetadataProvider : DataAnnotationsModelMetadataProvider
{
public static readonly string GroupNameKey = "__GroupName";
private static Guid _guid = new Guid("7d6d0de2-8d59-49ac-99cc-881423b75a76");
protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(
CachedDataAnnotationsModelMetadata prototype,
Func<object> modelAccessor)
{
var metadata = base.CreateMetadataFromPrototype(prototype, modelAccessor);
foreach (var keyValuePair in prototype.AdditionalValues)
{
metadata.AdditionalValues.Add(keyValuePair);
}
return metadata;
}
protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(
IEnumerable<object> attributes,
Type containerType,
Type modelType,
string propertyName)
{
var metadata = base.CreateMetadataPrototype(attributes, containerType, modelType, propertyName);
if (modelType == typeof(LargeModelWithValidation))
{
metadata.AdditionalValues.Add("key1", _guid);
metadata.AdditionalValues.Add("key2", "value2");
}
var displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault();
var groupName = displayAttribute?.GroupName;
if (!string.IsNullOrEmpty(groupName))
{
metadata.AdditionalValues[GroupNameKey] = groupName;
}
return metadata;
}
}
}

View File

@ -0,0 +1,39 @@
// 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.Collections.Generic;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ModelBinding;
using ModelBindingWebSite.Models;
using ModelBindingWebSite.ViewModels;
namespace ModelBindingWebSite.Controllers
{
public class ModelMetadataController
{
[HttpGet(template: "/AdditionalValues")]
public IDictionary<string, object> GetAdditionalValues([FromServices] IModelMetadataProvider provider)
{
var metadata = provider.GetMetadataForType(
modelAccessor: null,
modelType: typeof(LargeModelWithValidation));
return metadata.AdditionalValues;
}
[HttpGet(template: "/GroupNames")]
public IDictionary<string, string> GetGroupNames([FromServices] IModelMetadataProvider provider)
{
var groupNames = new Dictionary<string, string>();
var metadata = provider.GetMetadataForType(
modelAccessor: null,
modelType: typeof(VehicleViewModel));
foreach (var property in metadata.Properties)
{
groupNames.Add(property.PropertyName, property.GetGroupName());
}
return groupNames;
}
}
}

View File

@ -0,0 +1,26 @@
// 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.ModelBinding;
namespace ModelBindingWebSite
{
/// <summary>
/// Extensions for <see cref="ModelMetadata"/>.
/// </summary>
public static class ModelMetadataExtensions
{
/// <summary>
/// Gets the group name associated with given <paramref name="modelMetadata"/>.
/// </summary>
/// <param name="modelMetadata">The <see cref="ModelMetadata"/></param>
/// <returns>Group name associated with given <paramref name="modelMetadata"/>.</returns>
public static string GetGroupName(this ModelMetadata modelMetadata)
{
object groupName;
modelMetadata.AdditionalValues.TryGetValue(AdditionalValuesMetadataProvider.GroupNameKey, out groupName);
return groupName as string;
}
}
}

View File

@ -3,6 +3,7 @@
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.DependencyInjection;
using ModelBindingWebSite.Services;
@ -17,6 +18,10 @@ namespace ModelBindingWebSite
// Set up application services
app.UseServices(services =>
{
// Override the IModelMetadataProvider AddMvc would normally use. Make service a singleton though
// AddMvc would configure DataAnnotationsModelMetadataProvider as transient.
services.AddSingleton<IModelMetadataProvider, AdditionalValuesMetadataProvider>();
// Add MVC services to the services container
services.AddMvc(configuration)
.Configure<MvcOptions>(m =>

View File

@ -15,10 +15,10 @@ namespace ModelBindingWebSite.ViewModels
[StringLength(8)]
public string Vin { get; set; }
[Display(Order = 1)]
[Display(Order = 1, GroupName = "MakeAndModelGroup")]
public string Make { get; set; }
[Display(Order = 0)]
[Display(Order = 0, GroupName = "MakeAndModelGroup")]
public string Model { get; set; }
// Placed using default Order (10000).
@ -31,7 +31,7 @@ namespace ModelBindingWebSite.ViewModels
[MaxLength(10)]
public DateTimeOffset[] InspectedDates { get; set; }
[Display(Order = 20000)]
[Display(Order = 20000, GroupName = "TrackingIdGroup")]
public string LastUpdatedTrackingId { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)