Jo Shields a575963da9 Imported Upstream version 3.6.0
Former-commit-id: da6be194a6b1221998fc28233f2503bd61dd9d14
2014-08-13 10:39:27 +01:00

486 lines
23 KiB
C#

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Data.Spatial;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using System.Data.Metadata.Edm;
namespace LinqFunctionStubsGenerator
{
/// <summary>
/// Class that writes the files using the input function metadata.
/// </summary>
class FunctionStubFileWriter
{
private readonly IEnumerable<EdmFunction> _functions;
private readonly Dictionary<String, String> _funcDictionaryToUse;
private readonly Dictionary<String, String> _paramDictionaryToUse;
/// <summary>
/// Initializes member fields.
/// </summary>
/// <param name="functions">Metadata about functions</param>
/// <param name="functionNames">Dictionary containing better function names</param>
/// <param name="parameterNames">Dictionary containing better parameter names</param>
public FunctionStubFileWriter(IEnumerable<EdmFunction> functions, Dictionary<String, String> functionNames, Dictionary<String, String> parameterNames)
{
_functions = functions;
_funcDictionaryToUse = functionNames;
_paramDictionaryToUse = parameterNames;
}
/// <summary>
/// Generates code and writes to the specified file.
/// </summary>
/// <param name="destinationFile">Filepath of the destination file</param>
/// <param name="namespaceString">Namespace where the class will reside</param>
/// <param name="className">Generated class name</param>
/// <param name="attributeNamespace">The 'EdmFunction' attribute parameter</param>
/// <param name="pascalCaseFunctionNames">If the input function names need to be pascal cased.</param>
public void GenerateToFile(String destinationFile, string namespaceString, string className, string attributeNamespace, Boolean pascalCaseFunctionNames)
{
//Use passed in class information to generate the class definition.
StringWriter newCode = GenerateCode(namespaceString, className, attributeNamespace, pascalCaseFunctionNames);
//Write to file.
try
{
using (StreamWriter writer = new StreamWriter(destinationFile, false))
{
writer.Write(newCode.ToString());
writer.Close();
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
/// <summary>
/// Main code generator method.
/// </summary>
/// <param name="namespaceString">Namespace where the class will reside</param>
/// <param name="className">Generated class name</param>
/// <param name="attributeNamespace">The 'EdmFunction' attribute parameter</param>
/// <param name="pascalCaseFunctionNames">If the input function names need to be pascal cased.</param>
public StringWriter GenerateCode(string namespaceString, string className, string attributeNamespace, Boolean pascalCaseFunctionNames)
{
StringWriter newCode = new StringWriter();
Boolean isAggregateFunction;
bool hasSByteParameterOrReturnType;
bool hasStringInParameterName;
String separator;
GenerateFileHeader(newCode, className);
GenerateUsingStatements(newCode);
newCode.WriteLine("namespace " + namespaceString);
newCode.WriteLine("{");
GenerateClassHeader(newCode, className, attributeNamespace);
foreach (System.Data.Metadata.Edm.EdmFunction function in _functions)
{
isAggregateFunction = false;
hasSByteParameterOrReturnType = false;
hasStringInParameterName = false;
separator = "";
String functionNameToUse = FindCorrectFunctionName(function.Name, pascalCaseFunctionNames);
GenerateFunctionHeader(newCode,attributeNamespace,function.Name);
Type returnType = ((PrimitiveType)(function.ReturnParameter.TypeUsage.EdmType)).ClrEquivalentType;
//Suppress warning that 'SByte' is not CLS-compliant.
if (returnType == typeof(SByte))
{
hasSByteParameterOrReturnType = true;
}
StringBuilder functionSignatureString = new StringBuilder();
AppendSpaces(functionSignatureString, 8);
functionSignatureString.Append("public static ");
WriteType(functionSignatureString, returnType);
functionSignatureString.Append(functionNameToUse + "(");
ReadOnlyMetadataCollection<FunctionParameter> functionParameters = function.Parameters;
Type parameterType;
foreach (System.Data.Metadata.Edm.FunctionParameter parameter in functionParameters)
{
String parameterNameToUse = parameter.Name;
parameterNameToUse = FindCorrectParameterName(parameterNameToUse);
//Detect aggregate functions. They have just one parameter and so stub can be generated here.
if (parameter.TypeUsage.EdmType.GetType() == typeof(System.Data.Metadata.Edm.CollectionType))
{
isAggregateFunction = true;
if (parameterNameToUse.ToLowerInvariant().Contains("string"))
{
hasStringInParameterName = true;
}
System.Data.Metadata.Edm.CollectionType collectionType = (System.Data.Metadata.Edm.CollectionType)parameter.TypeUsage.EdmType;
parameterType = ((PrimitiveType)(collectionType.TypeUsage.EdmType)).ClrEquivalentType;
//Detect if there is an 'SByte' parameter to suppress non-CLS-compliance warning.
//Generate the attribute only once for each function.
if (parameterType == typeof(SByte))
{
hasSByteParameterOrReturnType = true;
}
//Generate stub for non-nullable input parameters
functionSignatureString.Append("IEnumerable<" + parameterType.ToString());
//Supress fxcop message and CLS non-compliant attributes
GenerateFunctionAttributes(newCode, hasStringInParameterName, hasSByteParameterOrReturnType);
//Use the constructed function signature
newCode.Write(functionSignatureString.ToString());
GenerateAggregateFunctionStub(newCode,parameterType, returnType, parameterNameToUse, false);
//Generate stub for nullable input parameters
//Special Case: Do not generate nullable stub for input parameter of types Byte[]
//and String, since they are nullable.
if (!IsNullableType(parameterType))
{
GenerateFunctionHeader(newCode, attributeNamespace, function.Name);
//Supress fxcop message and CLS non-compliant attributes
GenerateFunctionAttributes(newCode, hasStringInParameterName, hasSByteParameterOrReturnType);
//Use the constructed function signature
newCode.Write(functionSignatureString.ToString());
GenerateAggregateFunctionStub(newCode, parameterType, returnType, parameterNameToUse, true);
}
} //End of processing parameters for aggregate functions.
//Process each parameter in case of non-aggregate functions.
else
{
parameterType = ((PrimitiveType)(parameter.TypeUsage.EdmType)).ClrEquivalentType;
functionSignatureString.Append(separator);
WriteType(functionSignatureString, parameterType);
functionSignatureString.Append(parameterNameToUse);
separator = ", ";
//Detect if there is an 'SByte' parameter to suppress non-CLS-compliance warning.
if (parameterType == typeof(SByte))
{
hasSByteParameterOrReturnType = true;
}
if (parameterNameToUse.ToLowerInvariant().Contains("string"))
{
hasStringInParameterName = true;
}
}
} //End for each parameter
//Generate stub for Non-aggregate functions after all input parameters are found.
if (!isAggregateFunction)
{
//Supress fxcop supression and CLS non-compliant attributes
GenerateFunctionAttributes(newCode, hasStringInParameterName, hasSByteParameterOrReturnType);
newCode.WriteLine(functionSignatureString.ToString() + ")");
AppendSpaces(newCode, 8);
newCode.WriteLine("{");
WriteExceptionStatement(newCode);
}
} //End for each function
AppendSpaces(newCode, 4);
newCode.WriteLine("}");
newCode.WriteLine("}");
newCode.Close();
return newCode;
}
/// <summary>
/// Generates the content for aggregate function stubs.
/// </summary>
/// <param name="newCode">Buffer used to store the code constructed so far</param>
/// <param name="parameterType">Type of parameter</param>
/// <param name="returnType">Return type of function</param>
/// <param name="parameterNameToUse">Parameter name</param>
/// <param name="isNullable">If this invokation is for generating the nullable/non-nullable stub</param>
private void GenerateAggregateFunctionStub(StringWriter newCode, Type parameterType, Type returnType, string parameterNameToUse, bool isNullable)
{
GenerateQuestionMark(newCode, isNullable);
newCode.Write("> ");
newCode.WriteLine(parameterNameToUse + ")");
AppendSpaces(newCode, 8);
newCode.WriteLine("{");
AppendSpaces(newCode, 12);
newCode.Write("ObjectQuery<" + parameterType.ToString());
GenerateQuestionMark(newCode, isNullable);
newCode.Write("> objectQuerySource = " + parameterNameToUse);
newCode.Write(" as ObjectQuery<" + parameterType.ToString());
GenerateQuestionMark(newCode, isNullable);
newCode.WriteLine(">;");
AppendSpaces(newCode, 12);
newCode.WriteLine("if (objectQuerySource != null)");
AppendSpaces(newCode, 12);
newCode.WriteLine("{");
AppendSpaces(newCode, 16);
newCode.Write("return ((IQueryable)objectQuerySource).Provider.Execute<" + returnType.ToString());
//Special case: Byte[], String are nullable
if (!IsNullableType(returnType))
{
newCode.Write("?");
}
newCode.Write(">(Expression.Call((MethodInfo)MethodInfo.GetCurrentMethod(),Expression.Constant(" + parameterNameToUse);
newCode.WriteLine(")));");
AppendSpaces(newCode, 12);
newCode.WriteLine("}");
WriteExceptionStatement(newCode);
}
/// <summary>
/// Writes a question mark in generated code for nullable parameters
/// </summary>
/// <param name="newCode">Buffer used to store the code constructed so far</param>
/// <param name="isNullable">Is this invokation for generating the nullable/non-nullable stub</param>
public void GenerateQuestionMark(StringWriter newCode, bool isNullable)
{
if (isNullable)
{
newCode.Write("?");
}
}
/// <summary>
/// Generates fxcop suppression and CLS non-compliant attributes.
/// </summary>
/// <param name="newCode"></param>
/// <param name="hasStringInParameterName"></param>
/// <param name="hasSByteParameterOrReturnType"></param>
private void GenerateFunctionAttributes(StringWriter newCode, bool hasStringInParameterName, bool hasSByteParameterOrReturnType)
{
//Supress fxcop message about 'string' in argument names.
if (hasStringInParameterName)
{
GenerateFxcopSuppressionAttribute(newCode);
}
//Suppress warning that 'SByte' is not CLS-compliant, generate the attribute only once.
if (hasSByteParameterOrReturnType)
{
GenerateSByteCLSNonComplaintAttribute(newCode);
}
}
/// <summary>
/// Generates the output file header.
/// </summary>
/// <param name="newCode">Buffer used to store the code constructed so far</param>
/// <param name="className">Generated class name</param>
private void GenerateFileHeader(StringWriter newCode, String className)
{
DateTime theTime = DateTime.Now;
newCode.WriteLine("//------------------------------------------------------------------------------");
newCode.WriteLine("// <auto-generated>");
newCode.WriteLine("// This code was generated by a tool.");
newCode.Write("// Generation date and time : ");
newCode.WriteLine(theTime.Date.ToShortDateString() + " " + theTime.TimeOfDay.ToString());
newCode.WriteLine("//");
newCode.WriteLine("// Changes to this file will be lost if the code is regenerated.");
newCode.WriteLine("// </auto-generated>");
newCode.WriteLine("//------------------------------------------------------------------------------");
newCode.WriteLine();
}
/// <summary>
/// Generates 'using' statements in the output C# file.
/// </summary>
/// <param name="newCode">Buffer used to store the code constructed so far</param>
private void GenerateUsingStatements(StringWriter newCode)
{
newCode.WriteLine(@"using System;");
newCode.WriteLine(@"using System.Collections.Generic;");
newCode.WriteLine(@"using System.Data.Objects;");
newCode.WriteLine(@"using System.Data.Objects.DataClasses;");
newCode.WriteLine(@"using System.Linq;");
newCode.WriteLine(@"using System.Linq.Expressions;");
newCode.WriteLine(@"using System.Reflection;");
newCode.WriteLine();
}
/// <summary>
/// Generates the function header comment and 'EdmFunction' attribute for every function.
/// </summary>
/// <param name="newCode">Buffer used to store the code constructed so far</param>
/// <param name="attributeNamespace">Namespace parameter to the 'EdmFunction' attribute</param>
/// <param name="functionName">Function name</param>
private void GenerateFunctionHeader(StringWriter newCode, String attributeNamespace, String functionName)
{
AppendSpaces(newCode, 8);
newCode.WriteLine("/// <summary>");
AppendSpaces(newCode, 8);
newCode.WriteLine("/// Proxy for the function " + attributeNamespace + "." + functionName);
AppendSpaces(newCode, 8);
newCode.WriteLine("/// </summary>");
AppendSpaces(newCode, 8);
newCode.WriteLine("[EdmFunction(\""+attributeNamespace+"\", \""+functionName+"\")]");
}
/// <summary>
/// Writes the function attribute to suppress warnings about non-CLS compliance due
/// to SByte arguments.
/// </summary>
/// <param name="newCode">Buffer used to store the code constructed so far</param>
private void GenerateSByteCLSNonComplaintAttribute(StringWriter newCode)
{
AppendSpaces(newCode, 8);
newCode.WriteLine("[CLSCompliant(false)]");
}
/// <summary>
/// Writes the function attribute to suppress 'fxcop' errors about argument names
/// like 'string1','characterString', etc.
/// </summary>
/// <param name="newCode">Buffer used to store the code constructed so far</param>
private void GenerateFxcopSuppressionAttribute(StringWriter newCode)
{
AppendSpaces(newCode, 8);
newCode.WriteLine("[System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Naming\", \"CA1720:IdentifiersShouldNotContainTypeNames\", MessageId = \"string\")]");
}
/// <summary>
/// Returns if the given data type is nullable.
/// </summary>
/// <param name="parameterType">Input data type</param>
/// <returns>True of input type is nullable, false otherwise</returns>
private Boolean IsNullableType(Type parameterType)
{
return parameterType == typeof (Byte[]) ||
parameterType == typeof (String) ||
typeof (DbGeometry).IsAssignableFrom(parameterType) ||
typeof (DbGeography).IsAssignableFrom(parameterType);
}
/// <summary>
/// Generates type information in code according to whether it is nullable
/// </summary>
/// <param name="code">Buffer used to store the code constructed so far</param>
/// <param name="parameterType">Input type</param>
private void WriteType(StringBuilder code, Type parameterType)
{
code.Append(parameterType.ToString());
if (!IsNullableType(parameterType))
{
code.Append("?");
}
code.Append(" ");
}
/// <summary>
/// Generates the exception statement which is thrown from each function stub.
/// </summary>
/// <param name="code">Buffer used to store the code constructed so far</param>
private void WriteExceptionStatement(StringWriter code)
{
AppendSpaces(code, 12);
code.WriteLine("throw new NotSupportedException(\"This function can only be invoked from LINQ to Entities.\");");
AppendSpaces(code, 8);
code.WriteLine("}");
code.WriteLine();
}
/// <summary>
/// Appends spaces to code line, this is avoid tabs as required by coding standards.
/// </summary>
/// <param name="str">Buffer used to store the code constructed so far</param>
/// <param name="num">Number of desired spaces</param>
private void AppendSpaces(StringWriter str, int num)
{
for (int i = 0; i < num; i++)
{
str.Write(" ");
}
}
/// <summary>
/// Appends spaces to code line, this is avoid tabs as required by coding standards.
/// </summary>
/// <param name="str">Buffer used to store the code constructed so far</param>
/// <param name="num">Number of desired spaces</param>
private void AppendSpaces(StringBuilder str, int num)
{
for (int i = 0; i < num; i++)
{
str.Append(" ");
}
}
/// <summary>
/// Looks up dictionary to find better function name(that fxcop likes). If not in dictionary
/// Pascal cases it if asked to.
/// </summary>
/// <param name="inputName">Function name from metadata</param>
/// <param name="pascalCaseFunctionNames">If required to Pascal case function name</param>
/// <returns>Better function name</returns>
private String FindCorrectFunctionName(String inputName, Boolean pascalCaseFunctionNames)
{
if (_funcDictionaryToUse == null)
{
return inputName;
}
String value;
if (_funcDictionaryToUse.TryGetValue(inputName, out value))
{
return value;
}
else if (pascalCaseFunctionNames)
{
String interFunctionName = inputName.ToLower();
char[] charFuncName = interFunctionName.ToCharArray();
charFuncName[0] = System.Char.ToUpper(charFuncName[0]);
return new String(charFuncName);
}
return inputName;
}
/// <summary>
/// Looks up dictionary to find better parameter name(that fxcop likes and one that is friendlier).
/// </summary>
/// <param name="inputName">Parameter name from metadata</param>
/// <returns>Better parameter name</returns>
private String FindCorrectParameterName(String inputParameterName)
{
String value;
if (_paramDictionaryToUse == null)
{
return inputParameterName;
}
if (_paramDictionaryToUse.TryGetValue(inputParameterName, out value))
{
return value;
}
return inputParameterName;
}
/// <summary>
/// Generates header for the class.
/// </summary>
/// <param name="newCode">Buffer used to store the code constructed so far</param>
/// <param name="className">Output class name</param>
public void GenerateClassHeader(StringWriter newCode, String className, String namespaceString)
{
AppendSpaces(newCode, 4);
newCode.WriteLine("/// <summary>");
AppendSpaces(newCode, 4);
newCode.Write("/// Contains function stubs that expose " +namespaceString);
newCode.WriteLine(" methods in Linq to Entities.");
AppendSpaces(newCode, 4);
newCode.WriteLine("/// </summary>");
AppendSpaces(newCode, 4);
newCode.Write("public static ");
if (className.Equals("EntityFunctions", StringComparison.Ordinal))
{
newCode.Write("partial ");
}
newCode.WriteLine("class " + className);
AppendSpaces(newCode, 4);
newCode.WriteLine("{");
}
}
}