// 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);
}
}
}