2 // System.Web.Compilation.AppCodeCompiler: A compiler for the App_Code folder
5 // Marek Habersack (grendello@gmail.com)
7 // (C) 2006 Marek Habersack
11 // Permission is hereby granted, free of charge, to any person obtaining
12 // a copy of this software and associated documentation files (the
13 // "Software"), to deal in the Software without restriction, including
14 // without limitation the rights to use, copy, modify, merge, publish,
15 // distribute, sublicense, and/or sell copies of the Software, and to
16 // permit persons to whom the Software is furnished to do so, subject to
17 // the following conditions:
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
22 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33 using System.CodeDom.Compiler;
34 using System.Configuration;
35 using System.Collections;
36 using System.Collections.Generic;
37 using System.Collections.Specialized;
38 using System.Globalization;
40 using System.Reflection;
42 using System.Web.Configuration;
43 using System.Web.Profile;
44 using System.Web.Util;
46 namespace System.Web.Compilation
48 class AssemblyPathResolver
50 static Dictionary <string, string> assemblyCache;
52 static AssemblyPathResolver ()
54 assemblyCache = new Dictionary <string, string> ();
57 public static string GetAssemblyPath (string assemblyName)
59 lock (assemblyCache) {
60 if (assemblyCache.ContainsKey (assemblyName))
61 return assemblyCache [assemblyName];
64 Exception error = null;
65 if (assemblyName.IndexOf (',') != -1) {
67 asm = Assembly.Load (assemblyName);
68 } catch (Exception e) {
75 asm = Assembly.LoadWithPartialName (assemblyName);
76 } catch (Exception e) {
82 throw new HttpException (String.Format ("Unable to find assembly {0}", assemblyName), error);
84 string path = new Uri (asm.CodeBase).LocalPath;
85 assemblyCache.Add (assemblyName, path);
91 internal class AppCodeAssembly
94 List<CodeCompileUnit> units;
99 string outputAssemblyName;
101 public string OutputAssemblyName
104 return outputAssemblyName;
110 get { return validAssembly; }
113 public string SourcePath
124 public List<string> Files
126 get { return files; }
130 public AppCodeAssembly (string name, string path)
132 this.files = new List<string> ();
133 this.units = new List<CodeCompileUnit> ();
134 this.validAssembly = true;
139 public void AddFile (string path)
144 public void AddUnit (CodeCompileUnit unit)
149 object OnCreateTemporaryAssemblyFile (string path)
151 FileStream f = new FileStream (path, FileMode.CreateNew);
156 // Build and add the assembly to the BuildManager's
157 // CodeAssemblies collection
158 public void Build (string[] binAssemblies)
160 Type compilerProvider = null;
161 CompilerInfo compilerInfo = null, cit;
162 string extension, language, cpfile = null;
163 List<string> knownfiles = new List<string>();
164 List<string> unknownfiles = new List<string>();
166 // First make sure all the files are in the same
169 foreach (string f in files) {
173 extension = Path.GetExtension (f);
174 if (String.IsNullOrEmpty (extension) || !CodeDomProvider.IsDefinedExtension (extension))
177 language = CodeDomProvider.GetLanguageFromExtension(extension);
178 if (!CodeDomProvider.IsDefinedLanguage (language))
181 if (!known || language == null) {
182 unknownfiles.Add (f);
186 cit = CodeDomProvider.GetCompilerInfo (language);
187 if (cit == null || !cit.IsCodeDomProviderTypeValid)
189 if (compilerProvider == null) {
191 compilerProvider = cit.CodeDomProviderType;
193 } else if (compilerProvider != cit.CodeDomProviderType)
194 throw new HttpException (
196 "Files {0} and {1} are in different languages - they cannot be compiled into the same assembly",
197 Path.GetFileName (cpfile),
198 Path.GetFileName (f)));
202 CodeDomProvider provider = null;
203 CompilationSection compilationSection = WebConfigurationManager.GetWebApplicationSection ("system.web/compilation") as CompilationSection;
204 if (compilerInfo == null) {
205 if (!CodeDomProvider.IsDefinedLanguage (compilationSection.DefaultLanguage))
206 throw new HttpException ("Failed to retrieve default source language");
207 compilerInfo = CodeDomProvider.GetCompilerInfo (compilationSection.DefaultLanguage);
208 if (compilerInfo == null || !compilerInfo.IsCodeDomProviderTypeValid)
209 throw new HttpException ("Internal error while initializing application");
212 provider = compilerInfo.CreateProvider ();
213 if (provider == null)
214 throw new HttpException ("A code provider error occurred while initializing application.");
216 AssemblyBuilder abuilder = new AssemblyBuilder (provider);
217 foreach (string file in knownfiles)
218 abuilder.AddCodeFile (file);
219 foreach (CodeCompileUnit unit in units)
220 abuilder.AddCodeCompileUnit (unit);
222 BuildProvider bprovider;
223 CompilerParameters parameters = compilerInfo.CreateDefaultCompilerParameters ();
224 parameters.IncludeDebugInformation = compilationSection.Debug;
226 if (binAssemblies != null && binAssemblies.Length > 0) {
227 StringCollection parmRefAsm = parameters.ReferencedAssemblies;
228 foreach (string binAsm in binAssemblies) {
229 if (parmRefAsm.Contains (binAsm))
232 parmRefAsm.Add (binAsm);
236 if (compilationSection != null) {
237 foreach (AssemblyInfo ai in compilationSection.Assemblies)
238 if (ai.Assembly != "*") {
240 parameters.ReferencedAssemblies.Add (
241 AssemblyPathResolver.GetAssemblyPath (ai.Assembly));
242 } catch (Exception ex) {
243 throw new HttpException (
244 String.Format ("Could not find assembly {0}.", ai.Assembly),
249 BuildProviderCollection buildProviders = compilationSection.BuildProviders;
251 foreach (string file in unknownfiles) {
252 bprovider = GetBuildProviderFor (file, buildProviders);
253 if (bprovider == null)
255 bprovider.GenerateCode (abuilder);
259 if (knownfiles.Count == 0 && unknownfiles.Count == 0 && units.Count == 0)
262 outputAssemblyName = (string)FileUtils.CreateTemporaryFile (
263 AppDomain.CurrentDomain.SetupInformation.DynamicBase,
264 name, "dll", OnCreateTemporaryAssemblyFile);
265 parameters.OutputAssembly = outputAssemblyName;
266 foreach (Assembly a in BuildManager.TopLevelAssemblies)
267 parameters.ReferencedAssemblies.Add (a.Location);
268 CompilerResults results = abuilder.BuildAssembly (parameters);
272 if (results.NativeCompilerReturnValue == 0) {
273 BuildManager.CodeAssemblies.Add (results.CompiledAssembly);
274 BuildManager.TopLevelAssemblies.Add (results.CompiledAssembly);
275 HttpRuntime.WritePreservationFile (results.CompiledAssembly, name);
277 if (HttpContext.Current.IsCustomErrorEnabled)
278 throw new HttpException ("An error occurred while initializing application.");
279 throw new CompilationException (null, results.Errors, null);
283 VirtualPath PhysicalToVirtual (string file)
285 return new VirtualPath (file.Replace (HttpRuntime.AppDomainAppPath, "~/").Replace (Path.DirectorySeparatorChar, '/'));
288 BuildProvider GetBuildProviderFor (string file, BuildProviderCollection buildProviders)
290 if (file == null || file.Length == 0 || buildProviders == null || buildProviders.Count == 0)
293 BuildProvider ret = buildProviders.GetProviderInstanceForExtension (Path.GetExtension (file));
294 if (ret != null && IsCorrectBuilderType (ret)) {
295 ret.SetVirtualPath (PhysicalToVirtual (file));
302 bool IsCorrectBuilderType (BuildProvider bp)
309 type = bp.GetType ();
310 attrs = type.GetCustomAttributes (true);
314 BuildProviderAppliesToAttribute bpAppliesTo;
315 bool attributeFound = false;
316 foreach (object attr in attrs) {
317 bpAppliesTo = attr as BuildProviderAppliesToAttribute;
318 if (bpAppliesTo == null)
320 attributeFound = true;
321 if ((bpAppliesTo.AppliesTo & BuildProviderAppliesTo.All) == BuildProviderAppliesTo.All ||
322 (bpAppliesTo.AppliesTo & BuildProviderAppliesTo.Code) == BuildProviderAppliesTo.Code)
333 internal class AppCodeCompiler
335 static bool _alreadyCompiled;
336 internal static string DefaultAppCodeAssemblyName;
338 // A dictionary that contains an entry per an assembly that will
339 // be produced by compiling App_Code. There's one main assembly
340 // and an optional number of assemblies as defined by the
341 // codeSubDirectories sub-element of the compilation element in
342 // the system.web section of the app's config file.
343 // Each entry's value is an AppCodeAssembly instance.
345 // Assemblies are named as follows:
347 // 1. main assembly: App_Code.{HASH}
348 // 2. subdir assemblies: App_SubCode_{DirName}.{HASH}
350 // If any of the assemblies contains files that would be
351 // compiled with different compilers, a System.Web.HttpException
354 // Files for which there is no explicit builder are ignored
357 // Files for which exist BuildProviders but which have no
358 // unambiguous language assigned to them (e.g. .wsdl files), are
359 // built using the default website compiler.
360 List<AppCodeAssembly> assemblies;
361 string providerTypeName = null;
363 public AppCodeCompiler ()
365 assemblies = new List<AppCodeAssembly>();
368 bool ProcessAppCodeDir (string appCode, AppCodeAssembly defasm)
370 // First process the codeSubDirectories
371 CompilationSection cs = (CompilationSection) WebConfigurationManager.GetWebApplicationSection ("system.web/compilation");
375 for (int i = 0; i < cs.CodeSubDirectories.Count; i++) {
376 aname = String.Concat ("App_SubCode_", cs.CodeSubDirectories[i].DirectoryName);
377 assemblies.Add (new AppCodeAssembly (
379 Path.Combine (appCode, cs.CodeSubDirectories[i].DirectoryName)));
383 return CollectFiles (appCode, defasm);
386 CodeTypeReference GetProfilePropertyType (string type)
388 if (String.IsNullOrEmpty (type))
389 throw new ArgumentException ("String size cannot be 0", "type");
390 return new CodeTypeReference (type);
393 string FindProviderTypeName (ProfileSection ps, string providerName)
395 if (ps.Providers == null || ps.Providers.Count == 0)
398 ProviderSettings pset = ps.Providers [providerName];
404 void GetProfileProviderAttribute (ProfileSection ps, CodeAttributeDeclarationCollection collection,
407 if (String.IsNullOrEmpty (providerName))
408 providerTypeName = FindProviderTypeName (ps, ps.DefaultProvider);
410 providerTypeName = FindProviderTypeName (ps, providerName);
411 if (providerTypeName == null)
412 throw new HttpException (String.Format ("Profile provider type not defined: {0}",
416 new CodeAttributeDeclaration (
418 new CodeAttributeArgument (
419 new CodePrimitiveExpression (providerTypeName)
425 void GetProfileSettingsSerializeAsAttribute (ProfileSection ps, CodeAttributeDeclarationCollection collection,
426 SerializationMode mode)
428 string parameter = String.Concat ("SettingsSerializeAs.", mode.ToString ());
430 new CodeAttributeDeclaration (
431 "SettingsSerializeAs",
432 new CodeAttributeArgument (
433 new CodeSnippetExpression (parameter)
440 void AddProfileClassGetProfileMethod (CodeTypeDeclaration profileClass)
442 CodeMethodReferenceExpression mref = new CodeMethodReferenceExpression (
443 new CodeTypeReferenceExpression (typeof (System.Web.Profile.ProfileBase)),
445 CodeMethodInvokeExpression minvoke = new CodeMethodInvokeExpression (
447 new CodeExpression[] { new CodeVariableReferenceExpression ("username") }
449 CodeCastExpression cast = new CodeCastExpression ();
450 cast.TargetType = new CodeTypeReference ("ProfileCommon");
451 cast.Expression = minvoke;
453 CodeMethodReturnStatement ret = new CodeMethodReturnStatement ();
454 ret.Expression = cast;
456 CodeMemberMethod method = new CodeMemberMethod ();
457 method.Name = "GetProfile";
458 method.ReturnType = new CodeTypeReference ("ProfileCommon");
459 method.Parameters.Add (new CodeParameterDeclarationExpression("System.String", "username"));
460 method.Statements.Add (ret);
461 method.Attributes = MemberAttributes.Public;
463 profileClass.Members.Add (method);
466 void AddProfileClassProperty (ProfileSection ps, CodeTypeDeclaration profileClass, ProfilePropertySettings pset)
468 string name = pset.Name;
469 if (String.IsNullOrEmpty (name))
470 throw new HttpException ("Profile property 'Name' attribute cannot be null.");
471 CodeMemberProperty property = new CodeMemberProperty ();
472 string typeName = pset.Type;
473 if (typeName == "string")
474 typeName = "System.String";
475 property.Name = name;
476 property.Type = GetProfilePropertyType (typeName);
477 property.Attributes = MemberAttributes.Public;
479 CodeAttributeDeclarationCollection collection = new CodeAttributeDeclarationCollection();
480 GetProfileProviderAttribute (ps, collection, pset.Provider);
481 GetProfileSettingsSerializeAsAttribute (ps, collection, pset.SerializeAs);
483 property.CustomAttributes = collection;
484 CodeMethodReturnStatement ret = new CodeMethodReturnStatement ();
485 CodeCastExpression cast = new CodeCastExpression ();
486 ret.Expression = cast;
488 CodeMethodReferenceExpression mref = new CodeMethodReferenceExpression (
489 new CodeThisReferenceExpression (),
491 CodeMethodInvokeExpression minvoke = new CodeMethodInvokeExpression (
493 new CodeExpression[] { new CodePrimitiveExpression (name) }
495 cast.TargetType = new CodeTypeReference (typeName);
496 cast.Expression = minvoke;
497 property.GetStatements.Add (ret);
499 if (!pset.ReadOnly) {
500 mref = new CodeMethodReferenceExpression (
501 new CodeThisReferenceExpression (),
503 minvoke = new CodeMethodInvokeExpression (
505 new CodeExpression[] { new CodePrimitiveExpression (name), new CodeSnippetExpression ("value") }
507 property.SetStatements.Add (minvoke);
511 profileClass.Members.Add (property);
514 void AddProfileClassGroupProperty (string groupName, string memberName, CodeTypeDeclaration profileClass)
516 CodeMemberProperty property = new CodeMemberProperty ();
517 property.Name = memberName;
518 property.Type = new CodeTypeReference (groupName);
519 property.Attributes = MemberAttributes.Public;
521 CodeMethodReturnStatement ret = new CodeMethodReturnStatement ();
522 CodeCastExpression cast = new CodeCastExpression ();
523 ret.Expression = cast;
525 CodeMethodReferenceExpression mref = new CodeMethodReferenceExpression (
526 new CodeThisReferenceExpression (),
528 CodeMethodInvokeExpression minvoke = new CodeMethodInvokeExpression (
530 new CodeExpression[] { new CodePrimitiveExpression (memberName) }
532 cast.TargetType = new CodeTypeReference (groupName);
533 cast.Expression = minvoke;
534 property.GetStatements.Add (ret);
536 profileClass.Members.Add (property);
539 void BuildProfileClass (ProfileSection ps, string className, ProfilePropertySettingsCollection psc,
540 CodeNamespace ns, string baseClass, bool baseIsGlobal,
541 SortedList <string, string> groupProperties)
543 CodeTypeDeclaration profileClass = new CodeTypeDeclaration (className);
544 CodeTypeReference cref = new CodeTypeReference (baseClass);
546 cref.Options |= CodeTypeReferenceOptions.GlobalReference;
547 profileClass.BaseTypes.Add (cref);
548 profileClass.TypeAttributes = TypeAttributes.Public;
549 ns.Types.Add (profileClass);
551 foreach (ProfilePropertySettings pset in psc)
552 AddProfileClassProperty (ps, profileClass, pset);
553 if (groupProperties != null && groupProperties.Count > 0)
554 foreach (KeyValuePair <string, string> group in groupProperties)
555 AddProfileClassGroupProperty (group.Key, group.Value, profileClass);
556 AddProfileClassGetProfileMethod (profileClass);
559 string MakeGroupName (string name)
561 return String.Concat ("ProfileGroup", name);
564 // FIXME: there should be some validation of syntactic correctness of the member/class name
565 // for the groups/properties. For now it's left to the compiler to report errors.
567 // CodeGenerator.IsValidLanguageIndependentIdentifier (id) - use that
569 bool ProcessCustomProfile (ProfileSection ps, AppCodeAssembly defasm)
571 CodeCompileUnit unit = new CodeCompileUnit ();
572 CodeNamespace ns = new CodeNamespace (null);
573 unit.Namespaces.Add (ns);
574 defasm.AddUnit (unit);
576 ns.Imports.Add (new CodeNamespaceImport ("System"));
577 ns.Imports.Add (new CodeNamespaceImport ("System.Configuration"));
578 ns.Imports.Add (new CodeNamespaceImport ("System.Web"));
579 ns.Imports.Add (new CodeNamespaceImport ("System.Web.Profile"));
581 RootProfilePropertySettingsCollection props = ps.PropertySettings;
585 SortedList<string, string> groupProperties = new SortedList<string, string> ();
587 foreach (ProfileGroupSettings pgs in props.GroupSettings) {
588 groupName = MakeGroupName (pgs.Name);
589 groupProperties.Add (groupName, pgs.Name);
590 BuildProfileClass (ps, groupName, pgs.PropertySettings, ns,
591 "System.Web.Profile.ProfileGroupBase", true, null);
594 string baseType = ps.Inherits;
595 if (String.IsNullOrEmpty (baseType))
596 baseType = "System.Web.Profile.ProfileBase";
598 string[] parts = baseType.Split (new char[] {','});
599 if (parts.Length > 1)
600 baseType = parts [0].Trim ();
604 if (baseType.IndexOf ('.') != -1)
607 baseIsGlobal = false;
609 BuildProfileClass (ps, "ProfileCommon", props, ns, baseType, baseIsGlobal, groupProperties);
613 // void PutCustomProfileInContext (HttpContext context, string assemblyName)
615 // Type type = Type.GetType (String.Format ("ProfileCommon, {0}",
616 // Path.GetFileNameWithoutExtension (assemblyName)));
617 // ProfileBase pb = Activator.CreateInstance (type) as ProfileBase;
619 // context.Profile = pb;
622 public static bool HaveCustomProfile (ProfileSection ps)
624 if (ps == null || !ps.Enabled)
627 RootProfilePropertySettingsCollection props = ps.PropertySettings;
628 ProfileGroupSettingsCollection groups = props != null ? props.GroupSettings : null;
630 if (!String.IsNullOrEmpty (ps.Inherits) || (props != null && props.Count > 0) || (groups != null && groups.Count > 0))
636 public void Compile ()
638 if (_alreadyCompiled)
641 string appCode = Path.Combine (HttpRuntime.AppDomainAppPath, "App_Code");
642 ProfileSection ps = WebConfigurationManager.GetWebApplicationSection ("system.web/profile") as ProfileSection;
643 bool haveAppCodeDir = Directory.Exists (appCode);
644 bool haveCustomProfile = HaveCustomProfile (ps);
646 if (!haveAppCodeDir && !haveCustomProfile)
649 AppCodeAssembly defasm = new AppCodeAssembly ("App_Code", appCode);
650 assemblies.Add (defasm);
652 bool haveCode = false;
654 haveCode = ProcessAppCodeDir (appCode, defasm);
655 if (haveCustomProfile)
656 if (ProcessCustomProfile (ps, defasm))
662 HttpRuntime.EnableAssemblyMapping (true);
663 string[] binAssemblies = HttpApplication.BinDirectoryAssemblies;
665 foreach (AppCodeAssembly aca in assemblies)
666 aca.Build (binAssemblies);
667 _alreadyCompiled = true;
668 DefaultAppCodeAssemblyName = Path.GetFileNameWithoutExtension (defasm.OutputAssemblyName);
672 if (haveCustomProfile && providerTypeName != null) {
673 if (Type.GetType (providerTypeName, false) == null) {
674 foreach (Assembly asm in BuildManager.TopLevelAssemblies) {
678 if (asm.GetType (providerTypeName, false) != null)
684 Exception noTypeException = null;
688 ptype = HttpApplication.LoadTypeFromBin (providerTypeName);
689 } catch (Exception ex) {
690 noTypeException = ex;
694 throw new HttpException (String.Format ("Profile provider type not found: {0}", providerTypeName), noTypeException);
698 // Documented (sort of...) briefly in:
700 // http://quickstarts.asp.net/QuickStartv20/aspnet/doc/extensibility.aspx
701 // http://msdn2.microsoft.com/en-us/library/system.web.hosting.virtualpathprovider.aspx
702 void RunAppInitialize ()
704 MethodInfo mi = null, tmi;
707 foreach (Assembly asm in BuildManager.CodeAssemblies) {
708 types = asm.GetExportedTypes ();
709 if (types == null || types.Length == 0)
712 foreach (Type type in types) {
713 tmi = type.GetMethod ("AppInitialize",
714 BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
722 throw new HttpException ("The static AppInitialize method found in more than one type in the App_Code directory.");
731 mi.Invoke (null, null);
734 bool CollectFiles (string dir, AppCodeAssembly aca)
736 bool haveFiles = false;
738 AppCodeAssembly curaca = aca;
739 foreach (string f in Directory.GetFiles (dir)) {
744 foreach (string d in Directory.GetDirectories (dir)) {
745 foreach (AppCodeAssembly a in assemblies)
746 if (a.SourcePath == d) {
750 if (CollectFiles (d, curaca))