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:
parent
b3c38bc573
commit
3303286288
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue