//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web.Compilation { using System; using System.Globalization; using System.CodeDom; using System.CodeDom.Compiler; using System.Collections.Specialized; using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Xml.Serialization; using System.Web.Hosting; using System.Web.UI; using System.Diagnostics; using System.Threading; using System.Text; using System.Web.Compilation.WCFModel; using System.Collections.Generic; using System.Web.Configuration; using System.Web.Resources; using System.Security; using System.Security.Permissions; using System.Data.Services.Design; using System.IO; using System.Xml; using System.Reflection; /// /// A build provider for WCF service references in ASP.NET projects. /// (Note: the ASMX version is called WebReferencesBuildProvider). /// Due to compatibility requirements, as few changes as possible were made /// to System.Web.dll, which contains the build manager and WebReferencesBuildProvider. /// WebReferencesBuildProvider will call into us for the App_WebReferences folder and /// each of its subdirectories (recursively) if it finds our assembly and type on the /// machine. /// Note: for a normal BuildProvider, the input file is represented by the protected VirtualPath /// property of the base. But for this build provider (same as for WebReferencesBuildProvider, the /// asmx web references build provider), the input is an entire directory, which is still represented /// by the protected VirtualPath property. /// TO DEBUG: The easiest way to debug is to debug the aspnet_compiler.exe command-line tool /// while it compiles a website with a .svcmap file in it. /// As an example, you could debug aspnet_compiler.exe with this command-line to debug /// one of the suites: /// /// /v MiddleService -p {path}\ddsuites\src\vs\vb\IndigoTools\BuildProvider\WCFBuildProvider1\WebSite\MiddleService -c c:\temp\output /// /// Important: it will only call the build provider if the sources in the website have changed or if you delete /// the deleted output folder's contents. /// /// Data services (Astoria): in order to support Astoria "data services" we added code /// to scan for "datasvcmap" files in addition to the existing "svcmap" files. For data services /// we call into the Astoria code-gen library to do the work instead of the regular indigo path. /// /// [ SuppressMessage("Microsoft.Naming", "CA1705:LongAcronymsShouldBePascalCased", Justification = "Too late to change in Orcas--the WCFBuildProvider is hard-coded in ASP.NET " + "build manager for app_webreferences folder. Build manager is part of redbits.") ] // Transparency [SecurityCritical] public class WCFBuildProvider : BuildProvider { internal const string WebRefDirectoryName = "App_WebReferences"; internal const string SvcMapExtension = ".svcmap"; internal const string DataSvcMapExtension = ".datasvcmap"; private const string TOOL_CONFIG_ITEM_NAME = "Reference.config"; /// /// version number for 3.5 framework /// private const int FRAMEWORK_VERSION_35 = 0x30005; /// /// Search through the folder represented by base.VirtualPath for .svcmap and .datasvcmap files. /// If any .svcmap/.datasvcmap files are found, then generate proxy code for them into the /// specified assemblyBuilder. /// /// Where to generate the proxy code /// /// When this routine is called, it is expected that the protected VirtualPath property has /// been set to the folder to scan for .svcmap/.datasvcmap files. /// [SecuritySafeCritical] public override void GenerateCode(AssemblyBuilder assemblyBuilder) { // Go through all the svcmap files in the directory VirtualDirectory vdir = GetVirtualDirectory(VirtualPath); foreach (VirtualFile child in vdir.Files) { string extension = IO.Path.GetExtension(child.VirtualPath); if (extension.Equals(SvcMapExtension, StringComparison.OrdinalIgnoreCase)) { // .svcmap file found // NOTE: the WebReferences code requires a physical path, so this feature // cannot work with a non-file based VirtualPathProvider string physicalPath = HostingEnvironment.MapPath(child.VirtualPath); CodeCompileUnit codeUnit = GenerateCodeFromServiceMapFile(physicalPath); // Add the CodeCompileUnit to the compilation assemblyBuilder.AddCodeCompileUnit(this, codeUnit); } else if (extension.Equals(DataSvcMapExtension, StringComparison.OrdinalIgnoreCase)) { // In .NET FX 3.5, the ADO.NET Data Service build provider was included as part of the // WCF build provider. In .NET FX 4.0, it is a separate build provider. However, under certain // circumstances (i.e. design time/Visual Studio), we may call the 4.0 version of the build provider // when we actually want the web site to target .NET FX 3.5, and in that case, we have to emulate the // old behavior. if (BuildManager.TargetFramework.Version.Major < 4) { // NOTE: the WebReferences code requires a physical path, so this feature // cannot work with a non-file based VirtualPathProvider string physicalPath = HostingEnvironment.MapPath(child.VirtualPath); GenerateCodeFromDataServiceMapFile(physicalPath, assemblyBuilder); } } } } /// /// Generate code for one .datasvcmap file /// /// The physical path to the data service map file private void GenerateCodeFromDataServiceMapFile(string mapFilePath, AssemblyBuilder assemblyBuilder) { try { assemblyBuilder.AddAssemblyReference(typeof(System.Data.Services.Client.DataServiceContext).Assembly); DataSvcMapFileLoader loader = new DataSvcMapFileLoader(mapFilePath); DataSvcMapFile mapFile = loader.LoadMapFile() as DataSvcMapFile; if (mapFile.MetadataList[0].ErrorInLoading != null) { throw mapFile.MetadataList[0].ErrorInLoading; } string edmxContent = mapFile.MetadataList[0].Content; System.Data.Services.Design.EntityClassGenerator generator = new System.Data.Services.Design.EntityClassGenerator(LanguageOption.GenerateCSharpCode); // the EntityClassGenerator works on streams/writers, does not return a CodeDom // object, so we use CreateCodeFile instead of compile units. using (TextWriter writer = assemblyBuilder.CreateCodeFile(this)) { // Note: currently GenerateCode never actually returns values // for the error case (even though it returns an IList of error // objects). Instead it throws on error. This may need some tweaking // later on. #if DEBUG object errors = #endif generator.GenerateCode( XmlReader.Create(new StringReader(edmxContent)), writer, GetGeneratedNamespace()); #if DEBUG Debug.Assert( errors == null || !(errors is ICollection) || ((ICollection)errors).Count == 0, "Errors reported through the return value. Expected an exception"); #endif writer.Flush(); } } catch (Exception ex) { string errorMessage = ex.Message; errorMessage = String.Format(CultureInfo.CurrentCulture, "{0}: {1}", IO.Path.GetFileName(mapFilePath), errorMessage); throw new InvalidOperationException(errorMessage, ex); } } /// /// Generate code for one .svcmap file /// /// the path to the service map file /// /// [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification="Violation is no longer relevant due to 4.0 CAS model")] private CodeCompileUnit GenerateCodeFromServiceMapFile(string mapFilePath) { try { string generatedNamespace = GetGeneratedNamespace(); SvcMapFileLoader loader = new SvcMapFileLoader(mapFilePath); SvcMapFile mapFile = loader.LoadMapFile() as SvcMapFile; HandleProxyGenerationErrors(mapFile.LoadErrors); // We always use C# for the generated proxy CodeDomProvider provider = System.CodeDom.Compiler.CodeDomProvider.CreateProvider("c#"); //Note: with the current implementation of the generator, it does all of its // work in the constructor. This may change in the future. VSWCFServiceContractGenerator generator = VSWCFServiceContractGenerator.GenerateCodeAndConfiguration( mapFile, GetToolConfig(mapFile, mapFilePath), provider, generatedNamespace, null, //targetConfiguration null, //configurationNamespace new ImportExtensionServiceProvider(), new TypeResolver(), FRAMEWORK_VERSION_35, typeof (System.Data.Design.TypedDataSetSchemaImporterExtensionFx35) //Always we are above framework version 3.5 ); // Determine what "name" to display to users for the service if there are any exceptions // If generatedNamespace is empty, then we display the name of the .svcmap file. string referenceDisplayName = String.IsNullOrEmpty(generatedNamespace) ? System.IO.Path.GetFileName(mapFilePath) : generatedNamespace; VerifyGeneratedCodeAndHandleErrors(referenceDisplayName, mapFile, generator.TargetCompileUnit, generator.ImportErrors, generator.ProxyGenerationErrors); #if DEBUG #if false IO.TextWriter writer = new IO.StringWriter(); CodeGeneratorOptions options = new CodeGeneratorOptions(); options.BlankLinesBetweenMembers=true; provider.GenerateCodeFromCompileUnit(generator.TargetCompileUnit, writer, options); Debug.WriteLine("Generated proxy code:\r\n" + writer.ToString()); #endif #endif return generator.TargetCompileUnit; } catch (Exception ex) { string errorMessage = ex.Message; errorMessage = String.Format(CultureInfo.CurrentCulture, "{0}: {1}", IO.Path.GetFileName(mapFilePath), errorMessage); throw new InvalidOperationException(errorMessage, ex); } } /// /// If there are errors passed in, handle them by throwing an appropriate error /// /// IEnumerable for easier unit test accessors private static void HandleProxyGenerationErrors(System.Collections.IEnumerable /**/ errors) { foreach (ProxyGenerationError generationError in errors) { // NOTE: the ASP.Net framework does not handle an error list, so we only give them the first error message // all warning messages are ignored today // // We treat all error messages from WsdlImport and ProxyGenerator as warning messages // The reason is that many of them are ignorable and doesn't block generating useful code. if (!generationError.IsWarning && generationError.ErrorGeneratorState != WCFModel.ProxyGenerationError.GeneratorState.GenerateCode) { throw new InvalidOperationException(ConvertToBuildProviderErrorMessage(generationError)); } } } /// /// Merge error message strings together /// /// /// /// /// private static void CollectErrorMessages(System.Collections.IEnumerable errors, StringBuilder collectedMessages) { foreach (ProxyGenerationError generationError in errors) { if (!generationError.IsWarning) { if (collectedMessages.Length > 0) { collectedMessages.Append(Environment.NewLine); } collectedMessages.Append(ConvertToBuildProviderErrorMessage(generationError)); } } } /// /// Format an error reported by the code generator to message string reported to the user. /// /// /// /// private static string ConvertToBuildProviderErrorMessage(ProxyGenerationError generationError) { string errorMessage = generationError.Message; if (!String.IsNullOrEmpty(generationError.MetadataFile)) { if (generationError.LineNumber < 0) { errorMessage = String.Format(CultureInfo.CurrentCulture, "'{0}': {1}", generationError.MetadataFile, errorMessage); } else if (generationError.LinePosition < 0) { errorMessage = String.Format(CultureInfo.CurrentCulture, "'{0}' ({1}): {2}", generationError.MetadataFile, generationError.LineNumber, errorMessage); } else { errorMessage = String.Format(CultureInfo.CurrentCulture, "'{0}' ({1},{2}): {3}", generationError.MetadataFile, generationError.LineNumber, generationError.LinePosition, errorMessage); } } return errorMessage; } /// /// Check the result from the code generator. /// By default we treat all error messages from WsdlImporter as warnings, /// and they will be ignored if valid code has been generated. /// We may hit other errors when we parse the metadata files. /// Those errors (which are usually because of a bad file) will not be ignored, because the user can fix them. /// If the WsdlImporter hasn't generated any code as we expect, we have to consider some of the error messages are fatal. /// We collect those messages and report to the user. /// /// The name of the generated reference /// Original Map File /// generated code compile unit /// /// /// private static void VerifyGeneratedCodeAndHandleErrors( string referenceDisplayName, SvcMapFile mapFile, CodeCompileUnit generatedCode, System.Collections.IEnumerable importErrors, System.Collections.IEnumerable generatorErrors) { // Check and report fatal error first... HandleProxyGenerationErrors(importErrors); HandleProxyGenerationErrors(generatorErrors); // if there is no fatal error, we expect valid type generated from the process // unless there is no metadata files, or there is a service contract type sharing if (mapFile.MetadataList.Count > 0 && mapFile.ClientOptions.ServiceContractMappingList.Count == 0) { if (!IsAnyTypeGenerated(generatedCode)) { StringBuilder collectedMessages = new StringBuilder(); // merge error messages CollectErrorMessages(importErrors, collectedMessages); CollectErrorMessages(generatorErrors, collectedMessages); throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WCFModelStrings.ReferenceGroup_FailedToGenerateCode, referenceDisplayName, collectedMessages.ToString())); } } } /// /// Check whether we have generated any types /// /// /// /// private static bool IsAnyTypeGenerated(CodeCompileUnit compileUnit) { if (compileUnit != null) { foreach (System.CodeDom.CodeNamespace codeNamespace in compileUnit.Namespaces) { if (codeNamespace.Types.Count > 0) { return true; } } } return false; } /// /// Retrieve a VirtualDirectory for the given virtual path /// /// /// private VirtualDirectory GetVirtualDirectory(string virtualPath) { return HostingEnvironment.VirtualPathProvider.GetDirectory(VirtualPath); } /// /// Caculate our namespace for current VirtualPath... /// /// /// private string GetGeneratedNamespace() { // First, determine the namespace to use for code generation. This is based on the // relative path of the reference from its base App_WebReferences directory // ... Get the virtual path to the App_WebReferences folder, e.g "/MyApp/App_WebReferences" string rootWebRefDirVirtualPath = GetWebRefDirectoryVirtualPath(); // ... Get the folder's directory path, e.g "/MyApp/Application_WebReferences/Foo/Bar", // where we'll look for .svcmap files string currentSubfolderUnderWebReferences = this.VirtualPath; if (currentSubfolderUnderWebReferences == null) { Debug.Fail("Shouldn't be given a null virtual path"); throw new InvalidOperationException(); } return CalculateGeneratedNamespace(rootWebRefDirVirtualPath, currentSubfolderUnderWebReferences); } /// /// Determine the namespace to use for the proxy generation /// /// The path to the App_WebReferences folder /// The path to the current folder /// private static string CalculateGeneratedNamespace(string webReferencesRootVirtualPath, string virtualPath) { // ... Ensure both folders have trailing slashes webReferencesRootVirtualPath = VirtualPathUtility.AppendTrailingSlash(webReferencesRootVirtualPath); virtualPath = VirtualPathUtility.AppendTrailingSlash(virtualPath); Debug.Assert(virtualPath.StartsWith(webReferencesRootVirtualPath, StringComparison.OrdinalIgnoreCase), "We expected to be inside the App_WebReferences folder"); // ... Determine the namespace to use, based on the directory structure where the .svcmap file // is found. if (webReferencesRootVirtualPath.Length == virtualPath.Length) { Debug.Assert(string.Equals(webReferencesRootVirtualPath, virtualPath, StringComparison.OrdinalIgnoreCase), "We expected to be in the App_WebReferences directory"); // If it's the root WebReferences dir, use the empty namespace return String.Empty; } else { // We're in a subdirectory of App_WebReferences. // Get the directory's relative path from App_WebReferences, e.g. "Foo/Bar" virtualPath = VirtualPathUtility.RemoveTrailingSlash(virtualPath).Substring(webReferencesRootVirtualPath.Length); // Split it into chunks separated by '/' string[] chunks = virtualPath.Split('/'); // Turn all the relevant chunks into valid namespace chunks for (int i = 0; i < chunks.Length; i++) { chunks[i] = MakeValidTypeNameFromString(chunks[i]); } // Put the relevant chunks back together to form the namespace return String.Join(".", chunks); } } /// /// Returns the app domain's application virtual path [from HttpRuntime.AppDomainAppVPath]. /// Includes trailing slash, e.g. "/MyApp/" /// private static string GetAppDomainAppVirtualPath() { string appVirtualPath = HttpRuntime.AppDomainAppVirtualPath; if (appVirtualPath == null) { Debug.Fail("Shouldn't get a null app virtual path from the app domain"); throw new InvalidOperationException(); } return VirtualPathUtility.AppendTrailingSlash(VirtualPathUtility.ToAbsolute(appVirtualPath)); } /// /// Gets the virtual path to the application's App_WebReferences directory, e.g. "/MyApp/App_WebReferences/" /// private static string GetWebRefDirectoryVirtualPath() { return VirtualPathUtility.Combine(GetAppDomainAppVirtualPath(), WebRefDirectoryName + @"\"); } /// /// Return a valid type name from a string by changing any character /// that's not a letter or a digit to an '_'. /// /// /// internal static string MakeValidTypeNameFromString(string typeName) { if (String.IsNullOrEmpty(typeName)) throw new ArgumentNullException("typeName"); StringBuilder sb = new StringBuilder(); for (int i = 0; i < typeName.Length; i++) { // Make sure it doesn't start with a digit (ASURT 31134) if (i == 0 && Char.IsDigit(typeName[0])) sb.Append('_'); if (Char.IsLetterOrDigit(typeName[i])) sb.Append(typeName[i]); else sb.Append('_'); } // Identifier can't be a single underscore character string validTypeName = sb.ToString(); if (validTypeName.Equals("_", StringComparison.Ordinal)) { validTypeName = "__"; } return validTypeName; } /// /// Get the appropriate tool configuration for this service reference. /// /// If a reference.config file is present, the configuration object returned /// will be the merged view of: /// /// Machine Config /// ReferenceConfig /// /// If not reference.config file is present, the configuration object returned /// will be a merged view of: /// /// Machine.config /// web.config in application's physical path... /// /// /// SvcMapFile representing the service /// private System.Configuration.Configuration GetToolConfig(SvcMapFile mapFile, string mapFilePath) { string toolConfigFile = null; if (mapFile != null && mapFilePath != null) { foreach (ExtensionFile extensionFile in mapFile.Extensions) { if (String.Equals(extensionFile.Name, TOOL_CONFIG_ITEM_NAME, StringComparison.Ordinal)) { toolConfigFile = extensionFile.FileName; } } } System.Web.Configuration.WebConfigurationFileMap fileMap; fileMap = new System.Web.Configuration.WebConfigurationFileMap(); System.Web.Configuration.VirtualDirectoryMapping mapping; if (toolConfigFile != null) { // // If we've got a specific tool configuration to use, we better load that... // mapping = new System.Web.Configuration.VirtualDirectoryMapping(System.IO.Path.GetDirectoryName(mapFilePath), true, toolConfigFile); } else { // // Otherwise we fall back to the default web.config file... // mapping = new System.Web.Configuration.VirtualDirectoryMapping(HostingEnvironment.ApplicationPhysicalPath, true); } fileMap.VirtualDirectories.Add("/", mapping); return System.Web.Configuration.WebConfigurationManager.OpenMappedWebConfiguration(fileMap, "/", System.Web.Hosting.HostingEnvironment.SiteName); } /// /// Helper class to implement type resolution for the generator /// private class TypeResolver : IContractGeneratorReferenceTypeLoader { private System.Reflection.Assembly[] _referencedAssemblies; private IEnumerable ReferencedAssemblies { get { if (_referencedAssemblies == null) { System.Collections.ICollection referencedAssemblyCollection = BuildManager.GetReferencedAssemblies(); _referencedAssemblies = new System.Reflection.Assembly[referencedAssemblyCollection.Count]; referencedAssemblyCollection.CopyTo(_referencedAssemblies, 0); } return _referencedAssemblies; } } [SecuritySafeCritical] System.Type IContractGeneratorReferenceTypeLoader.LoadType(string typeName) { // If the type can't be resolved, we need an exception thrown (thus the true argument) // so it can be reported as a build error. return BuildManager.GetType(typeName, true); } [SecuritySafeCritical] System.Reflection.Assembly IContractGeneratorReferenceTypeLoader.LoadAssembly(string assemblyName) { System.Reflection.AssemblyName assemblyToLookFor = new System.Reflection.AssemblyName(assemblyName); foreach (System.Reflection.Assembly assembly in ReferencedAssemblies) { if (System.Reflection.AssemblyName.ReferenceMatchesDefinition(assemblyToLookFor, assembly.GetName())) { return assembly; } } throw new System.IO.FileNotFoundException(String.Format(CultureInfo.CurrentCulture, WCFModelStrings.ReferenceGroup_FailedToLoadAssembly, assemblyName)); } [SecuritySafeCritical] void IContractGeneratorReferenceTypeLoader.LoadAllAssemblies(out IEnumerable loadedAssemblies, out IEnumerable loadingErrors) { loadedAssemblies = ReferencedAssemblies; loadingErrors = new System.Exception[] {}; } } [SuppressMessage("Microsoft.Usage", "CA2302:FlagServiceProviders", Justification = "IServiceProvider implementation does not return anything.")] private class ImportExtensionServiceProvider : IServiceProvider { #region IServiceProvider Members [SecuritySafeCritical] public object GetService(Type serviceType) { // We don't currently provide any services to import extensions in the build provider context return null; } #endregion } } }