Files
linux-packaging-mono/external/aspnetwebstack/src/Microsoft.Web.Http.Data.Helpers/UpshotExtensions.cs
Jo Shields a575963da9 Imported Upstream version 3.6.0
Former-commit-id: da6be194a6b1221998fc28233f2503bd61dd9d14
2014-08-13 10:39:27 +01:00

321 lines
14 KiB
C#

// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Mvc;
namespace Microsoft.Web.Http.Data.Helpers
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static class UpshotExtensions
{
public static UpshotConfigBuilder UpshotContext(this HtmlHelper htmlHelper)
{
return UpshotContext(htmlHelper, false);
}
public static UpshotConfigBuilder UpshotContext(this HtmlHelper htmlHelper, bool bufferChanges)
{
return new UpshotConfigBuilder(htmlHelper, bufferChanges);
}
}
public class UpshotConfigBuilder : IHtmlString
{
private readonly HtmlHelper htmlHelper;
private readonly bool bufferChanges;
private readonly IDictionary<string, IDataSourceConfig> dataSources = new Dictionary<string, IDataSourceConfig>();
private readonly IDictionary<Type, string> clientMappings = new Dictionary<Type, string>();
public UpshotConfigBuilder(HtmlHelper htmlHelper, bool bufferChanges)
{
this.htmlHelper = htmlHelper;
this.bufferChanges = bufferChanges;
}
private interface IDataSourceConfig
{
string ClientName { get; }
Type DataControllerType { get; }
string SharedDataContextExpression { get; }
string DataContextExpression { set; }
string ClientMappingsJson { set; }
string GetInitializationScript();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Following established design pattern for HTML helpers.")]
public UpshotConfigBuilder DataSource<TDataController>(Expression<Func<TDataController, object>> queryOperation) where TDataController : DataController
{
return this.DataSource<TDataController>(queryOperation, null, null);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "Following established design pattern for HTML helpers."),
System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Following established design pattern for HTML helpers.")]
public UpshotConfigBuilder DataSource<TDataController>(Expression<Func<TDataController, object>> queryOperation, string serviceUrl, string clientName) where TDataController : DataController
{
IDataSourceConfig dataSourceConfig = new DataSourceConfig<TDataController>(htmlHelper, bufferChanges, queryOperation, serviceUrl, clientName);
if (dataSources.ContainsKey(dataSourceConfig.ClientName))
{
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Cannot have multiple data sources with the same clientName. Found multiple data sources with the name '{0}'", dataSourceConfig.ClientName));
}
dataSources.Add(dataSourceConfig.ClientName, dataSourceConfig);
return this;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Following established design pattern for HTML helpers.")]
public UpshotConfigBuilder ClientMapping<TEntity>(string clientConstructor)
{
if (string.IsNullOrEmpty(clientConstructor))
{
throw new ArgumentException("clientConstructor cannot be null or empty", "clientConstructor");
}
if (clientMappings.ContainsKey(typeof(TEntity)))
{
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Cannot have multiple client mappings for the same entity type. Found multiple client mappings for '{0}'", typeof(TEntity).FullName));
}
clientMappings.Add(typeof(TEntity), clientConstructor);
return this;
}
public string ToHtmlString()
{
StringBuilder js = new StringBuilder("upshot.dataSources = upshot.dataSources || {};\n");
// First emit metadata for each referenced DataController
IEnumerable<Type> dataControllerTypes = dataSources.Select(x => x.Value.DataControllerType).Distinct();
foreach (Type dataControllerType in dataControllerTypes)
{
js.AppendFormat("upshot.metadata({0});\n", GetMetadata(dataControllerType));
}
// Let the first dataSource construct a dataContext, and all subsequent ones share it
IEnumerable<IDataSourceConfig> allDataSources = dataSources.Values;
IDataSourceConfig firstDataSource = allDataSources.FirstOrDefault();
if (firstDataSource != null)
{
// All but the first data source share the DataContext implicitly instantiated by the first.
foreach (IDataSourceConfig dataSource in allDataSources.Skip(1))
{
dataSource.DataContextExpression = firstDataSource.SharedDataContextExpression;
}
// Let the first dataSource define the client mappings
firstDataSource.ClientMappingsJson = GetClientMappingsObjectLiteral();
}
// Now emit initialization code for each dataSource
foreach (IDataSourceConfig dataSource in allDataSources)
{
js.AppendLine("\n" + dataSource.GetInitializationScript());
}
// Also record the mapping functions in use
foreach (var mapping in clientMappings)
{
js.AppendFormat("upshot.registerType(\"{0}\", function() {{ return {1} }});\n", EncodeServerTypeName(mapping.Key), mapping.Value);
}
return string.Format(CultureInfo.InvariantCulture, "<script type='text/javascript'>\n{0}</script>", js);
}
private string GetMetadata(Type dataControllerType)
{
var methodInfo = typeof(MetadataExtensions).GetMethod("Metadata");
var result = (IHtmlString)methodInfo.MakeGenericMethod(dataControllerType).Invoke(null, new[] { htmlHelper });
return result.ToHtmlString();
}
private string GetClientMappingsObjectLiteral()
{
IEnumerable<string> clientMappingStrings =
clientMappings.Select(
clientMapping => string.Format(CultureInfo.InvariantCulture, "\"{0}\": function(data) {{ return new {1}(data) }}", EncodeServerTypeName(clientMapping.Key), clientMapping.Value));
return string.Format(CultureInfo.InvariantCulture, "{{{0}}}", string.Join(",", clientMappingStrings));
}
// TODO: Duplicated from DataControllerMetadataGenerator.cs. Refactor when combining this into the main System.Web.Http.Data.Helper assembly.
private static string EncodeServerTypeName(Type type)
{
return String.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", type.Name, ":#", type.Namespace);
}
private class DataSourceConfig<TDataController> : IDataSourceConfig where TDataController : DataController
{
private readonly HtmlHelper htmlHelper;
private readonly bool bufferChanges;
private readonly Expression<Func<TDataController, object>> queryOperation;
private readonly string serviceUrlOverride;
private readonly string clientName;
public DataSourceConfig(HtmlHelper htmlHelper, bool bufferChanges, Expression<Func<TDataController, object>> queryOperation, string serviceUrlOverride, string clientName)
{
this.htmlHelper = htmlHelper;
this.bufferChanges = bufferChanges;
this.queryOperation = queryOperation;
this.serviceUrlOverride = serviceUrlOverride;
this.clientName = string.IsNullOrEmpty(clientName) ? DefaultClientName : clientName;
}
public string ClientName
{
get
{
return clientName;
}
}
public Type DataControllerType
{
get
{
return typeof(TDataController);
}
}
public string DataContextExpression { private get; set; }
public string ClientMappingsJson { private get; set; }
public string SharedDataContextExpression
{
get
{
return ClientExpression + ".getDataContext()";
}
}
private string ClientExpression
{
get
{
return "upshot.dataSources." + ClientName;
}
}
private Type EntityType
{
get
{
Type operationReturnType = OperationMethod.ReturnType;
Type genericTypeDefinition = operationReturnType.IsGenericType ? operationReturnType.GetGenericTypeDefinition() : null;
Type entityType;
if (genericTypeDefinition != null && (genericTypeDefinition == typeof(IQueryable<>) || genericTypeDefinition == typeof(IEnumerable<>)))
{
// Permits IQueryable<TEntity> and IEnumerable<TEntity>.
entityType = operationReturnType.GetGenericArguments().Single();
}
else
{
entityType = operationReturnType;
}
if (!Description.EntityTypes.Any(type => type == entityType))
{
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "queryOperation '{0}' must return an entity type or an IEnumerable/IQueryable of an entity type", OperationMethod.Name));
}
return entityType;
}
}
private string ServiceUrl
{
get
{
if (!string.IsNullOrEmpty(serviceUrlOverride))
{
return serviceUrlOverride;
}
UrlHelper urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
string dataControllerName = typeof(TDataController).Name;
if (!dataControllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("DataController type name must end with 'Controller'");
}
string controllerRouteName = dataControllerName.Substring(0, dataControllerName.Length - "Controller".Length);
return urlHelper.RouteUrl(new { controller = controllerRouteName, action = UrlParameter.Optional, httproute = true });
}
}
private string DefaultClientName
{
get
{
string operationName = OperationMethod.Name;
// By convention, strip away any "Get" verb on the method. Clients can override by explictly specifying client name.
return operationName.StartsWith("Get", StringComparison.OrdinalIgnoreCase) && operationName.Length > 3 && char.IsLetter(operationName[3]) ? operationName.Substring(3) : operationName;
}
}
private MethodInfo OperationMethod
{
get
{
Expression body = queryOperation.Body;
// The VB compiler will inject a convert to object here.
if (body.NodeType == ExpressionType.Convert)
{
UnaryExpression convert = (UnaryExpression)body;
if (convert.Type == typeof(object))
{
body = convert.Operand;
}
}
MethodCallExpression methodCall = body as MethodCallExpression;
if (methodCall == null)
{
throw new ArgumentException("queryOperation must be a method call");
}
if (!methodCall.Method.DeclaringType.IsAssignableFrom(typeof(TDataController)))
{
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "queryOperation must be a method on '{0}' or a base type", typeof(TDataController).Name));
}
return methodCall.Method;
}
}
private static DataControllerDescription Description
{
get
{
HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor
{
Configuration = GlobalConfiguration.Configuration, // This helper can't be run until after global app init.
ControllerType = typeof(TDataController)
};
DataControllerDescription description = DataControllerDescription.GetDescription(controllerDescriptor);
return description;
}
}
public string GetInitializationScript()
{
return string.Format(CultureInfo.InvariantCulture, @"{0} = upshot.RemoteDataSource({{
providerParameters: {{ url: ""{1}"", operationName: ""{2}"" }},
entityType: ""{3}"",
bufferChanges: {4},
dataContext: {5},
mapping: {6}
}});",
ClientExpression, ServiceUrl, OperationMethod.Name, EncodeServerTypeName(EntityType),
bufferChanges ? "true" : "false", DataContextExpression ?? "undefined", ClientMappingsJson ?? "undefined");
}
}
}
}