207 lines
8.9 KiB
C#
207 lines
8.9 KiB
C#
// 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.Globalization;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
|
|
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
|
{
|
|
public static class ExpressionMetadataProvider
|
|
{
|
|
public static ModelExplorer FromLambdaExpression<TModel, TResult>(
|
|
Expression<Func<TModel, TResult>> expression,
|
|
ViewDataDictionary<TModel> viewData,
|
|
IModelMetadataProvider metadataProvider)
|
|
{
|
|
if (expression == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(expression));
|
|
}
|
|
|
|
if (viewData == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(viewData));
|
|
}
|
|
|
|
string propertyName = null;
|
|
Type containerType = null;
|
|
var legalExpression = false;
|
|
|
|
// Need to verify the expression is valid; it needs to at least end in something
|
|
// that we can convert to a meaningful string for model binding purposes
|
|
|
|
switch (expression.Body.NodeType)
|
|
{
|
|
case ExpressionType.ArrayIndex:
|
|
// ArrayIndex always means a single-dimensional indexer;
|
|
// multi-dimensional indexer is a method call to Get().
|
|
legalExpression = true;
|
|
break;
|
|
|
|
case ExpressionType.Call:
|
|
// Only legal method call is a single argument indexer/DefaultMember call
|
|
legalExpression = ExpressionHelper.IsSingleArgumentIndexer(expression.Body);
|
|
break;
|
|
|
|
case ExpressionType.MemberAccess:
|
|
// Property/field access is always legal
|
|
var memberExpression = (MemberExpression)expression.Body;
|
|
propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : null;
|
|
if (string.Equals(propertyName, "Model", StringComparison.Ordinal) &&
|
|
memberExpression.Type == typeof(TModel) &&
|
|
memberExpression.Expression.NodeType == ExpressionType.Constant)
|
|
{
|
|
// Special case the Model property in RazorPage<TModel>. (m => Model) should behave identically
|
|
// to (m => m). But do the more complicated thing for (m => m.Model) since that is a slightly
|
|
// different beast.)
|
|
return FromModel(viewData, metadataProvider);
|
|
}
|
|
|
|
containerType = memberExpression.Expression.Type;
|
|
legalExpression = true;
|
|
break;
|
|
|
|
case ExpressionType.Parameter:
|
|
// Parameter expression means "model => model", so we delegate to FromModel
|
|
return FromModel(viewData, metadataProvider);
|
|
}
|
|
|
|
if (!legalExpression)
|
|
{
|
|
throw new InvalidOperationException(Resources.TemplateHelpers_TemplateLimitations);
|
|
}
|
|
|
|
Func<object, object> modelAccessor = (container) =>
|
|
{
|
|
try
|
|
{
|
|
return CachedExpressionCompiler.Process(expression)((TModel)container);
|
|
}
|
|
catch (NullReferenceException)
|
|
{
|
|
return null;
|
|
}
|
|
};
|
|
|
|
ModelMetadata metadata;
|
|
if (propertyName == null)
|
|
{
|
|
// Ex:
|
|
// m => 5 (arbitrary expression)
|
|
// m => foo (arbitrary expression)
|
|
// m => m.Widgets[0] (expression ending with non-property-access)
|
|
metadata = metadataProvider.GetMetadataForType(typeof(TResult));
|
|
}
|
|
else
|
|
{
|
|
// Ex:
|
|
// m => m.Color (simple property access)
|
|
// m => m.Color.Red (nested property access)
|
|
// m => m.Widgets[0].Size (expression ending with property-access)
|
|
metadata = metadataProvider.GetMetadataForType(containerType).Properties[propertyName];
|
|
}
|
|
|
|
return viewData.ModelExplorer.GetExplorerForExpression(metadata, modelAccessor);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets <see cref="ModelExplorer"/> for named <paramref name="expression"/> in given
|
|
/// <paramref name="viewData"/>.
|
|
/// </summary>
|
|
/// <param name="expression">Expression name, relative to <c>viewData.Model</c>.</param>
|
|
/// <param name="viewData">
|
|
/// The <see cref="ViewDataDictionary"/> that may contain the <paramref name="expression"/> value.
|
|
/// </param>
|
|
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
|
|
/// <returns>
|
|
/// <see cref="ModelExplorer"/> for named <paramref name="expression"/> in given <paramref name="viewData"/>.
|
|
/// </returns>
|
|
public static ModelExplorer FromStringExpression(
|
|
string expression,
|
|
ViewDataDictionary viewData,
|
|
IModelMetadataProvider metadataProvider)
|
|
{
|
|
if (viewData == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(viewData));
|
|
}
|
|
|
|
var viewDataInfo = ViewDataEvaluator.Eval(viewData, expression);
|
|
if (viewDataInfo == null)
|
|
{
|
|
// Try getting a property from ModelMetadata if we couldn't find an answer in ViewData
|
|
var propertyExplorer = viewData.ModelExplorer.GetExplorerForProperty(expression);
|
|
if (propertyExplorer != null)
|
|
{
|
|
return propertyExplorer;
|
|
}
|
|
}
|
|
|
|
if (viewDataInfo != null)
|
|
{
|
|
if (viewDataInfo.Container == viewData &&
|
|
viewDataInfo.Value == viewData.Model &&
|
|
string.IsNullOrEmpty(expression))
|
|
{
|
|
// Nothing for empty expression in ViewData and ViewDataEvaluator just returned the model. Handle
|
|
// using FromModel() for its object special case.
|
|
return FromModel(viewData, metadataProvider);
|
|
}
|
|
|
|
ModelExplorer containerExplorer = viewData.ModelExplorer;
|
|
if (viewDataInfo.Container != null)
|
|
{
|
|
containerExplorer = metadataProvider.GetModelExplorerForType(
|
|
viewDataInfo.Container.GetType(),
|
|
viewDataInfo.Container);
|
|
}
|
|
|
|
if (viewDataInfo.PropertyInfo != null)
|
|
{
|
|
// We've identified a property access, which provides us with accurate metadata.
|
|
var containerType = viewDataInfo.Container?.GetType() ?? viewDataInfo.PropertyInfo.DeclaringType;
|
|
var containerMetadata = metadataProvider.GetMetadataForType(viewDataInfo.Container.GetType());
|
|
var propertyMetadata = containerMetadata.Properties[viewDataInfo.PropertyInfo.Name];
|
|
|
|
Func<object, object> modelAccessor = (ignore) => viewDataInfo.Value;
|
|
return containerExplorer.GetExplorerForExpression(propertyMetadata, modelAccessor);
|
|
}
|
|
else if (viewDataInfo.Value != null)
|
|
{
|
|
// We have a value, even though we may not know where it came from.
|
|
var valueMetadata = metadataProvider.GetMetadataForType(viewDataInfo.Value.GetType());
|
|
return containerExplorer.GetExplorerForExpression(valueMetadata, viewDataInfo.Value);
|
|
}
|
|
}
|
|
|
|
// Treat the expression as string if we don't find anything better.
|
|
var stringMetadata = metadataProvider.GetMetadataForType(typeof(string));
|
|
return viewData.ModelExplorer.GetExplorerForExpression(stringMetadata, modelAccessor: null);
|
|
}
|
|
|
|
private static ModelExplorer FromModel(
|
|
ViewDataDictionary viewData,
|
|
IModelMetadataProvider metadataProvider)
|
|
{
|
|
if (viewData == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(viewData));
|
|
}
|
|
|
|
if (viewData.ModelMetadata.ModelType == typeof(object))
|
|
{
|
|
// Use common simple type rather than object so e.g. Editor() at least generates a TextBox.
|
|
var model = viewData.Model == null ? null : Convert.ToString(viewData.Model, CultureInfo.CurrentCulture);
|
|
return metadataProvider.GetModelExplorerForType(typeof(string), model);
|
|
}
|
|
else
|
|
{
|
|
return viewData.ModelExplorer;
|
|
}
|
|
}
|
|
}
|
|
}
|