// Copyright (c) .NET Foundation. 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.Diagnostics; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.ViewFeatures { /// /// Associates a model object with it's corresponding . /// [DebuggerDisplay("DeclaredType={Metadata.ModelType.Name} PropertyName={Metadata.PropertyName}")] public class ModelExplorer { private readonly IModelMetadataProvider _metadataProvider; private object _model; private Func _modelAccessor; private ModelExplorer[] _properties; /// /// Creates a new . /// /// The . /// The . /// The model object. May be null. public ModelExplorer( IModelMetadataProvider metadataProvider, ModelMetadata metadata, object model) { if (metadataProvider == null) { throw new ArgumentNullException(nameof(metadataProvider)); } if (metadata == null) { throw new ArgumentNullException(nameof(metadata)); } _metadataProvider = metadataProvider; Metadata = metadata; _model = model; } /// /// Creates a new . /// /// The . /// The container . /// The . /// A model accessor function.. public ModelExplorer( IModelMetadataProvider metadataProvider, ModelExplorer container, ModelMetadata metadata, Func modelAccessor) { if (metadataProvider == null) { throw new ArgumentNullException(nameof(metadataProvider)); } if (container == null) { throw new ArgumentNullException(nameof(container)); } if (metadata == null) { throw new ArgumentNullException(nameof(metadata)); } _metadataProvider = metadataProvider; Container = container; Metadata = metadata; _modelAccessor = modelAccessor; } /// /// Creates a new . /// /// The . /// The container . /// The . /// The model object. May be null. public ModelExplorer( IModelMetadataProvider metadataProvider, ModelExplorer container, ModelMetadata metadata, object model) { if (metadataProvider == null) { throw new ArgumentNullException(nameof(metadataProvider)); } if (metadata == null) { throw new ArgumentNullException(nameof(metadata)); } _metadataProvider = metadataProvider; Container = container; Metadata = metadata; _model = model; } /// /// Gets the container . /// /// /// /// The will most commonly be set as a result of calling /// . In this case, the returned will /// have it's set to the instance upon which /// was called. /// /// /// This however is not a requirement. The is informational, and may not /// represent a type that defines the property represented by . This can /// occur when constructing a based on evaluation of a complex /// expression. /// /// /// If calling code relies on a parent-child relationship between /// instances, then use to validate this assumption. /// /// public ModelExplorer Container { get; } /// /// Gets the . /// public ModelMetadata Metadata { get; } /// /// Gets the model object. /// /// /// Retrieving the object will execute the model accessor function if this /// was provided with one. /// public object Model { get { if (_model == null && _modelAccessor != null) { Debug.Assert(Container != null); _model = _modelAccessor(Container.Model); // Null-out the accessor so we don't invoke it repeatedly if it returns null. _modelAccessor = null; } return _model; } } /// /// Retrieving the will execute the model accessor function if this /// was provided with one. /// public Type ModelType { get { if (Metadata.IsNullableValueType) { // We have a model, but if it's a nullable value type, then Model.GetType() will return // the non-nullable type (int? -> int). Since it's a value type, there's no subclassing, // just go with the declared type. return Metadata.ModelType; } if (Model == null) { // If the model is null, then use the declared model type; return Metadata.ModelType; } // We have a model, and it's not a nullable, so use the runtime type to handle // cases where the model is a subclass of the declared type and has extra data. return Model.GetType(); } } /// /// Gets the properties. /// /// /// Includes a for each property of the /// for . /// public IEnumerable Properties => PropertiesInternal; private ModelExplorer[] PropertiesInternal { get { if (_properties == null) { var metadata = GetMetadataForRuntimeType(); var properties = metadata.Properties; var propertyHelpers = PropertyHelper.GetProperties(ModelType); _properties = new ModelExplorer[properties.Count]; for (var i = 0; i < properties.Count; i++) { var propertyMetadata = properties[i]; PropertyHelper propertyHelper = null; for (var j = 0; j < propertyHelpers.Length; j++) { if (string.Equals( propertyMetadata.PropertyName, propertyHelpers[j].Property.Name, StringComparison.Ordinal)) { propertyHelper = propertyHelpers[j]; break; } } Debug.Assert(propertyHelper != null); _properties[i] = CreateExplorerForProperty(propertyMetadata, propertyHelper); } } return _properties; } } /// /// Gets a for the given value. /// /// The model value. /// A . public ModelExplorer GetExplorerForModel(object model) { if (Container == null) { return new ModelExplorer(_metadataProvider, Metadata, model); } else { return new ModelExplorer(_metadataProvider, Container, Metadata, model); } } /// /// Gets a for the property with given , or null if /// the property cannot be found. /// /// The property name. /// A , or null. public ModelExplorer GetExplorerForProperty(string name) { if (name == null) { throw new ArgumentNullException(nameof(name)); } for (var i = 0; i < PropertiesInternal.Length; i++) { var property = PropertiesInternal[i]; if (string.Equals(name, property.Metadata.PropertyName, StringComparison.Ordinal)) { return property; } } return null; } /// /// Gets a for the property with given , or null if /// the property cannot be found. /// /// The property name. /// An accessor for the model value. /// A , or null. /// /// As this creates a model explorer with a specific model accessor function, the result is not cached. /// public ModelExplorer GetExplorerForProperty(string name, Func modelAccessor) { if (name == null) { throw new ArgumentNullException(nameof(name)); } var metadata = GetMetadataForRuntimeType(); var propertyMetadata = metadata.Properties[name]; if (propertyMetadata == null) { return null; } return new ModelExplorer(_metadataProvider, this, propertyMetadata, modelAccessor); } /// /// Gets a for the property with given , or null if /// the property cannot be found. /// /// The property name. /// The model value. /// A , or null. /// /// As this creates a model explorer with a specific model value, the result is not cached. /// public ModelExplorer GetExplorerForProperty(string name, object model) { if (name == null) { throw new ArgumentNullException(nameof(name)); } var metadata = GetMetadataForRuntimeType(); var propertyMetadata = metadata.Properties[name]; if (propertyMetadata == null) { return null; } return new ModelExplorer(_metadataProvider, this, propertyMetadata, model); } /// /// Gets a for the provided model value and model . /// /// The model . /// The model value. /// A . /// /// /// A created by /// represents the result of executing an arbitrary expression against the model contained /// in the current instance. /// /// /// The returned will have the current instance set as its . /// /// public ModelExplorer GetExplorerForExpression(Type modelType, object model) { if (modelType == null) { throw new ArgumentNullException(nameof(modelType)); } var metadata = _metadataProvider.GetMetadataForType(modelType); return GetExplorerForExpression(metadata, model); } /// /// Gets a for the provided model value and model . /// /// The associated with the model. /// The model value. /// A . /// /// /// A created by /// /// represents the result of executing an arbitrary expression against the model contained /// in the current instance. /// /// /// The returned will have the current instance set as its . /// /// public ModelExplorer GetExplorerForExpression(ModelMetadata metadata, object model) { if (metadata == null) { throw new ArgumentNullException(nameof(metadata)); } return new ModelExplorer(_metadataProvider, this, metadata, model); } /// /// Gets a for the provided model value and model . /// /// The model . /// A model accessor function. /// A . /// /// /// A created by /// /// represents the result of executing an arbitrary expression against the model contained /// in the current instance. /// /// /// The returned will have the current instance set as its . /// /// public ModelExplorer GetExplorerForExpression(Type modelType, Func modelAccessor) { if (modelType == null) { throw new ArgumentNullException(nameof(modelType)); } var metadata = _metadataProvider.GetMetadataForType(modelType); return GetExplorerForExpression(metadata, modelAccessor); } /// /// Gets a for the provided model value and model . /// /// The associated with the model. /// A model accessor function. /// A . /// /// /// A created by /// /// represents the result of executing an arbitrary expression against the model contained /// in the current instance. /// /// /// The returned will have the current instance set as its . /// /// public ModelExplorer GetExplorerForExpression(ModelMetadata metadata, Func modelAccessor) { if (metadata == null) { throw new ArgumentNullException(nameof(metadata)); } return new ModelExplorer(_metadataProvider, this, metadata, modelAccessor); } private ModelMetadata GetMetadataForRuntimeType() { // We want to make sure we're looking at the runtime properties of the model, and for // that we need the model metadata of the runtime type. var metadata = Metadata; if (Metadata.ModelType != ModelType) { metadata = _metadataProvider.GetMetadataForType(ModelType); } return metadata; } private ModelExplorer CreateExplorerForProperty( ModelMetadata propertyMetadata, PropertyHelper propertyHelper) { if (propertyHelper == null) { return new ModelExplorer(_metadataProvider, this, propertyMetadata, modelAccessor: null); } var modelAccessor = new Func((c) => { return c == null ? null : propertyHelper.GetValue(c); }); return new ModelExplorer(_metadataProvider, this, propertyMetadata, modelAccessor); } } }