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.Globalization;
39 using System.Reflection;
41 using System.Web.Configuration;
42 using System.Web.Profile;
43 using System.Web.Util;
45 namespace System.Web.Compilation
47 class AssemblyPathResolver
49 static Dictionary <string, string> assemblyCache;
51 static AssemblyPathResolver ()
53 assemblyCache = new Dictionary <string, string> ();
56 public static string GetAssemblyPath (string assemblyName)
58 lock (assemblyCache) {
59 if (assemblyCache.ContainsKey (assemblyName))
60 return assemblyCache [assemblyName];
63 Exception error = null;
64 if (assemblyName.IndexOf (',') != -1) {
66 asm = Assembly.Load (assemblyName);
67 } catch (Exception e) {
74 asm = Assembly.LoadWithPartialName (assemblyName);
75 } catch (Exception e) {
81 throw new HttpException (String.Format ("Unable to find assembly {0}", assemblyName), error);
83 assemblyCache.Add (assemblyName, asm.Location);
89 internal class AppCodeAssembly
91 private List<string> files;
92 private List<CodeCompileUnit> units;
96 private bool validAssembly;
97 private string outputAssemblyName;
99 public string OutputAssemblyName
102 return outputAssemblyName;
108 get { return validAssembly; }
111 public string SourcePath
122 public List<string> Files
124 get { return files; }
128 public AppCodeAssembly (string name, string path)
130 this.files = new List<string> ();
131 this.units = new List<CodeCompileUnit> ();
132 this.validAssembly = true;
137 public void AddFile (string path)
142 public void AddUnit (CodeCompileUnit unit)
147 object OnCreateTemporaryAssemblyFile (string path)
149 FileStream f = new FileStream (path, FileMode.CreateNew);
154 // Build and add the assembly to the BuildManager's
155 // CodeAssemblies collection
156 public void Build (string[] binAssemblies)
158 Type compilerProvider = null;
159 CompilerInfo compilerInfo = null, cit;
160 string extension, language, cpfile = null;
161 List<string> knownfiles = new List<string>();
162 List<string> unknownfiles = new List<string>();
164 // First make sure all the files are in the same
166 bool known = false, unknown = false;
167 foreach (string f in files) {
171 extension = Path.GetExtension (f);
172 if (String.IsNullOrEmpty (extension) || !CodeDomProvider.IsDefinedExtension (extension))
175 language = CodeDomProvider.GetLanguageFromExtension(extension);
176 if (!CodeDomProvider.IsDefinedLanguage (language))
179 if (!known || language == null) {
180 unknownfiles.Add (f);
184 cit = CodeDomProvider.GetCompilerInfo (language);
185 if (cit == null || !cit.IsCodeDomProviderTypeValid)
187 if (compilerProvider == null) {
189 compilerProvider = cit.CodeDomProviderType;
191 } else if (compilerProvider != cit.CodeDomProviderType)
192 throw new HttpException (
194 "Files {0} and {1} are in different languages - they cannot be compiled into the same assembly",
195 Path.GetFileName (cpfile),
196 Path.GetFileName (f)));
200 CodeDomProvider provider = null;
201 CompilationSection compilationSection = WebConfigurationManager.GetSection ("system.web/compilation") as CompilationSection;
202 if (compilerInfo == null) {
203 if (!CodeDomProvider.IsDefinedLanguage (compilationSection.DefaultLanguage))
204 throw new HttpException ("Failed to retrieve default source language");
205 compilerInfo = CodeDomProvider.GetCompilerInfo (compilationSection.DefaultLanguage);
206 if (compilerInfo == null || !compilerInfo.IsCodeDomProviderTypeValid)
207 throw new HttpException ("Internal error while initializing application");
208 provider = compilerInfo.CreateProvider ();
209 if (provider == null)
210 throw new HttpException ("A code provider error occurred while initializing application.");
213 provider = compilerInfo.CreateProvider ();
214 if (provider == null)
215 throw new HttpException ("A code provider error occurred while initializing application.");
217 AssemblyBuilder abuilder = new AssemblyBuilder (provider);
218 foreach (string file in knownfiles)
219 abuilder.AddCodeFile (file);
220 foreach (CodeCompileUnit unit in units)
221 abuilder.AddCodeCompileUnit (unit);
223 BuildProvider bprovider;
224 CompilerParameters parameters = compilerInfo.CreateDefaultCompilerParameters ();
225 parameters.IncludeDebugInformation = compilationSection.Debug;
227 if (binAssemblies != null && binAssemblies.Length > 0)
228 parameters.ReferencedAssemblies.AddRange (binAssemblies);
230 if (compilationSection != null) {
231 AssemblyName asmName;
232 foreach (AssemblyInfo ai in compilationSection.Assemblies)
233 if (ai.Assembly != "*") {
235 parameters.ReferencedAssemblies.Add (
236 AssemblyPathResolver.GetAssemblyPath (ai.Assembly));
237 } catch (Exception ex) {
238 throw new HttpException (
239 String.Format ("Could not find assembly {0}.", ai.Assembly),
244 BuildProviderCollection buildProviders = compilationSection.BuildProviders;
246 foreach (string file in unknownfiles) {
247 bprovider = GetBuildProviderFor (file, buildProviders);
248 if (bprovider == null)
250 bprovider.GenerateCode (abuilder);
255 if (knownfiles.Count == 0 && unknownfiles.Count == 0 && units.Count == 0)
258 outputAssemblyName = (string)FileUtils.CreateTemporaryFile (
259 AppDomain.CurrentDomain.SetupInformation.DynamicBase,
260 name, "dll", OnCreateTemporaryAssemblyFile);
261 parameters.OutputAssembly = outputAssemblyName;
262 foreach (Assembly a in BuildManager.TopLevelAssemblies)
263 parameters.ReferencedAssemblies.Add (a.Location);
264 CompilerResults results = abuilder.BuildAssembly (parameters);
265 if (results.NativeCompilerReturnValue == 0) {
266 BuildManager.CodeAssemblies.Add (results.CompiledAssembly);
267 BuildManager.TopLevelAssemblies.Add (results.CompiledAssembly);
268 HttpRuntime.WritePreservationFile (results.CompiledAssembly, name);
270 if (HttpContext.Current.IsCustomErrorEnabled)
271 throw new HttpException ("An error occurred while initializing application.");
272 throw new CompilationException (null, results.Errors, null);
276 private string PhysicalToVirtual (string file)
278 return file.Replace (HttpRuntime.AppDomainAppPath, "/").Replace (Path.DirectorySeparatorChar, '/');
281 private BuildProvider GetBuildProviderFor (string file, BuildProviderCollection buildProviders)
283 if (file == null || file.Length == 0 || buildProviders == null || buildProviders.Count == 0)
286 BuildProvider ret = buildProviders.GetProviderForExtension (Path.GetExtension (file));
287 if (ret != null && IsCorrectBuilderType (ret)) {
288 ret.SetVirtualPath (PhysicalToVirtual (file));
295 private bool IsCorrectBuilderType (BuildProvider bp)
302 type = bp.GetType ();
303 attrs = type.GetCustomAttributes (true);
307 BuildProviderAppliesToAttribute bpAppliesTo;
308 bool attributeFound = false;
309 foreach (object attr in attrs) {
310 bpAppliesTo = attr as BuildProviderAppliesToAttribute;
311 if (bpAppliesTo == null)
313 attributeFound = true;
314 if ((bpAppliesTo.AppliesTo & BuildProviderAppliesTo.All) == BuildProviderAppliesTo.All ||
315 (bpAppliesTo.AppliesTo & BuildProviderAppliesTo.Code) == BuildProviderAppliesTo.Code)
326 internal class AppCodeCompiler
328 static private bool _alreadyCompiled;
329 internal static string DefaultAppCodeAssemblyName;
331 // A dictionary that contains an entry per an assembly that will
332 // be produced by compiling App_Code. There's one main assembly
333 // and an optional number of assemblies as defined by the
334 // codeSubDirectories sub-element of the compilation element in
335 // the system.web section of the app's config file.
336 // Each entry's value is an AppCodeAssembly instance.
338 // Assemblies are named as follows:
340 // 1. main assembly: App_Code.{HASH}
341 // 2. subdir assemblies: App_SubCode_{DirName}.{HASH}
343 // If any of the assemblies contains files that would be
344 // compiled with different compilers, a System.Web.HttpException
347 // Files for which there is no explicit builder are ignored
350 // Files for which exist BuildProviders but which have no
351 // unambiguous language assigned to them (e.g. .wsdl files), are
352 // built using the default website compiler.
353 private List<AppCodeAssembly> assemblies;
354 string _bindir = null;
355 string providerTypeName = null;
357 public AppCodeCompiler ()
359 assemblies = new List<AppCodeAssembly>();
362 bool ProcessAppCodeDir (string appCode, AppCodeAssembly defasm)
364 // First process the codeSubDirectories
365 CompilationSection cs = (CompilationSection) WebConfigurationManager.GetSection ("system.web/compilation");
369 for (int i = 0; i < cs.CodeSubDirectories.Count; i++) {
370 aname = String.Format ("App_SubCode_{0}", cs.CodeSubDirectories[i].DirectoryName);
371 assemblies.Add (new AppCodeAssembly (
373 Path.Combine (appCode, cs.CodeSubDirectories[i].DirectoryName)));
377 return CollectFiles (appCode, defasm);
380 CodeTypeReference GetProfilePropertyType (string type)
382 if (String.IsNullOrEmpty (type))
383 throw new ArgumentException ("String size cannot be 0", "type");
384 return new CodeTypeReference (type);
387 Type GetTypeFromBin (string typeName)
389 string bindir = BinDir;
390 if (!Directory.Exists (bindir))
393 string [] binDlls = Directory.GetFiles (bindir, "*.dll");
395 foreach (string dll in binDlls) {
397 Assembly asm = Assembly.LoadFrom (dll);
398 ret = asm.GetType (typeName, false);
401 } catch (Exception) {
409 string FindProviderTypeName (ProfileSection ps, string providerName)
411 if (ps.Providers == null || ps.Providers.Count == 0)
414 ProviderSettings pset = ps.Providers [providerName];
420 void GetProfileProviderAttribute (ProfileSection ps, CodeAttributeDeclarationCollection collection,
423 if (String.IsNullOrEmpty (providerName))
424 providerTypeName = FindProviderTypeName (ps, ps.DefaultProvider);
426 providerTypeName = FindProviderTypeName (ps, providerName);
427 if (providerTypeName == null)
428 throw new HttpException (String.Format ("Profile provider type not defined: {0}",
432 new CodeAttributeDeclaration (
434 new CodeAttributeArgument (
435 new CodePrimitiveExpression (providerTypeName)
441 void GetProfileSettingsSerializeAsAttribute (ProfileSection ps, CodeAttributeDeclarationCollection collection,
442 SerializationMode mode)
444 string parameter = String.Format ("SettingsSerializeAs.{0}", mode.ToString ());
446 new CodeAttributeDeclaration (
447 "SettingsSerializeAs",
448 new CodeAttributeArgument (
449 new CodeSnippetExpression (parameter)
456 void AddProfileClassGetProfileMethod (CodeTypeDeclaration profileClass)
458 CodeMethodReferenceExpression mref = new CodeMethodReferenceExpression (
459 new CodeTypeReferenceExpression (typeof (System.Web.Profile.ProfileBase)),
461 CodeMethodInvokeExpression minvoke = new CodeMethodInvokeExpression (
463 new CodeExpression[] { new CodeVariableReferenceExpression ("username") }
465 CodeCastExpression cast = new CodeCastExpression ();
466 cast.TargetType = new CodeTypeReference ("ProfileCommon");
467 cast.Expression = minvoke;
469 CodeMethodReturnStatement ret = new CodeMethodReturnStatement ();
470 ret.Expression = cast;
472 CodeMemberMethod method = new CodeMemberMethod ();
473 method.Name = "GetProfile";
474 method.ReturnType = new CodeTypeReference ("ProfileCommon");
475 method.Parameters.Add (new CodeParameterDeclarationExpression("System.String", "username"));
476 method.Statements.Add (ret);
477 method.Attributes = MemberAttributes.Public;
479 profileClass.Members.Add (method);
482 void AddProfileClassProperty (ProfileSection ps, CodeTypeDeclaration profileClass, ProfilePropertySettings pset)
484 string name = pset.Name;
485 if (String.IsNullOrEmpty (name))
486 throw new HttpException ("Profile property 'Name' attribute cannot be null.");
487 CodeMemberProperty property = new CodeMemberProperty ();
488 string typeName = pset.Type;
489 if (typeName == "string")
490 typeName = "System.String";
491 property.Name = name;
492 property.Type = GetProfilePropertyType (typeName);
493 property.Attributes = MemberAttributes.Public;
495 CodeAttributeDeclarationCollection collection = new CodeAttributeDeclarationCollection();
496 GetProfileProviderAttribute (ps, collection, pset.Provider);
497 GetProfileSettingsSerializeAsAttribute (ps, collection, pset.SerializeAs);
499 property.CustomAttributes = collection;
500 CodeMethodReturnStatement ret = new CodeMethodReturnStatement ();
501 CodeCastExpression cast = new CodeCastExpression ();
502 ret.Expression = cast;
504 CodeMethodReferenceExpression mref = new CodeMethodReferenceExpression (
505 new CodeThisReferenceExpression (),
507 CodeMethodInvokeExpression minvoke = new CodeMethodInvokeExpression (
509 new CodeExpression[] { new CodePrimitiveExpression (name) }
511 cast.TargetType = new CodeTypeReference (typeName);
512 cast.Expression = minvoke;
513 property.GetStatements.Add (ret);
515 if (!pset.ReadOnly) {
516 mref = new CodeMethodReferenceExpression (
517 new CodeThisReferenceExpression (),
519 minvoke = new CodeMethodInvokeExpression (
521 new CodeExpression[] { new CodePrimitiveExpression (name), new CodeSnippetExpression ("value") }
523 property.SetStatements.Add (minvoke);
527 profileClass.Members.Add (property);
530 void AddProfileClassGroupProperty (string groupName, string memberName, CodeTypeDeclaration profileClass)
532 CodeMemberProperty property = new CodeMemberProperty ();
533 property.Name = memberName;
534 property.Type = new CodeTypeReference (groupName);
535 property.Attributes = MemberAttributes.Public;
537 CodeMethodReturnStatement ret = new CodeMethodReturnStatement ();
538 CodeCastExpression cast = new CodeCastExpression ();
539 ret.Expression = cast;
541 CodeMethodReferenceExpression mref = new CodeMethodReferenceExpression (
542 new CodeThisReferenceExpression (),
544 CodeMethodInvokeExpression minvoke = new CodeMethodInvokeExpression (
546 new CodeExpression[] { new CodePrimitiveExpression (memberName) }
548 cast.TargetType = new CodeTypeReference (groupName);
549 cast.Expression = minvoke;
550 property.GetStatements.Add (ret);
552 profileClass.Members.Add (property);
555 void BuildProfileClass (ProfileSection ps, string className, ProfilePropertySettingsCollection psc,
556 CodeNamespace ns, string baseClass, bool baseIsGlobal,
557 SortedList <string, string> groupProperties)
559 CodeTypeDeclaration profileClass = new CodeTypeDeclaration (className);
560 CodeTypeReference cref = new CodeTypeReference (baseClass);
562 cref.Options |= CodeTypeReferenceOptions.GlobalReference;
563 profileClass.BaseTypes.Add (cref);
564 profileClass.TypeAttributes = TypeAttributes.Public;
565 ns.Types.Add (profileClass);
567 foreach (ProfilePropertySettings pset in psc)
568 AddProfileClassProperty (ps, profileClass, pset);
569 if (groupProperties != null && groupProperties.Count > 0)
570 foreach (KeyValuePair <string, string> group in groupProperties)
571 AddProfileClassGroupProperty (group.Key, group.Value, profileClass);
572 AddProfileClassGetProfileMethod (profileClass);
575 string MakeGroupName (string name)
577 return String.Format ("ProfileGroup{0}", name);
580 // FIXME: there should be some validation of syntactic correctness of the member/class name
581 // for the groups/properties. For now it's left to the compiler to report errors.
583 // CodeGenerator.IsValidLanguageIndependentIdentifier (id) - use that
585 bool ProcessCustomProfile (ProfileSection ps, AppCodeAssembly defasm)
587 CodeCompileUnit unit = new CodeCompileUnit ();
588 CodeNamespace ns = new CodeNamespace (null);
589 unit.Namespaces.Add (ns);
590 defasm.AddUnit (unit);
592 ns.Imports.Add (new CodeNamespaceImport ("System"));
593 ns.Imports.Add (new CodeNamespaceImport ("System.Configuration"));
594 ns.Imports.Add (new CodeNamespaceImport ("System.Web"));
595 ns.Imports.Add (new CodeNamespaceImport ("System.Web.Profile"));
597 RootProfilePropertySettingsCollection props = ps.PropertySettings;
601 SortedList<string, string> groupProperties = new SortedList<string, string> ();
603 foreach (ProfileGroupSettings pgs in props.GroupSettings) {
604 groupName = MakeGroupName (pgs.Name);
605 groupProperties.Add (groupName, pgs.Name);
606 BuildProfileClass (ps, groupName, pgs.PropertySettings, ns,
607 "System.Web.Profile.ProfileGroupBase", true, null);
610 string baseType = ps.Inherits;
611 bool baseIsGlobal = false;
612 if (String.IsNullOrEmpty (baseType)) {
613 baseType = "System.Web.Profile.ProfileBase";
617 BuildProfileClass (ps, "ProfileCommon", props, ns, baseType, baseIsGlobal, groupProperties);
621 // void PutCustomProfileInContext (HttpContext context, string assemblyName)
623 // Type type = Type.GetType (String.Format ("ProfileCommon, {0}",
624 // Path.GetFileNameWithoutExtension (assemblyName)));
625 // ProfileBase pb = Activator.CreateInstance (type) as ProfileBase;
627 // context.Profile = pb;
630 public static bool HaveCustomProfile (ProfileSection ps)
632 if (ps == null || !ps.Enabled)
634 if (!String.IsNullOrEmpty (ps.Inherits) || (ps.PropertySettings != null && ps.PropertySettings.Count > 0))
640 public void Compile ()
642 if (_alreadyCompiled)
645 string appCode = Path.Combine (HttpRuntime.AppDomainAppPath, "App_Code");
646 ProfileSection ps = WebConfigurationManager.GetSection ("system.web/profile") as ProfileSection;
647 bool haveAppCodeDir = Directory.Exists (appCode);
648 bool haveCustomProfile = HaveCustomProfile (ps);
650 if (!haveAppCodeDir && !haveCustomProfile)
653 AppCodeAssembly defasm = new AppCodeAssembly ("App_Code", appCode);
654 assemblies.Add (defasm);
656 bool haveCode = false;
658 haveCode = ProcessAppCodeDir (appCode, defasm);
659 if (haveCustomProfile)
660 if (ProcessCustomProfile (ps, defasm))
666 HttpRuntime.EnableAssemblyMapping (true);
667 string bindir = BinDir;
668 string[] binAssemblies = null;
669 if (Directory.Exists (bindir))
670 binAssemblies = Directory.GetFiles (bindir, "*.dll");
671 foreach (AppCodeAssembly aca in assemblies)
672 aca.Build (binAssemblies);
673 _alreadyCompiled = true;
674 DefaultAppCodeAssemblyName = Path.GetFileNameWithoutExtension (defasm.OutputAssemblyName);
676 if (haveCustomProfile && providerTypeName != null) {
677 if (Type.GetType (providerTypeName, false) == null) {
678 foreach (Assembly asm in BuildManager.TopLevelAssemblies) {
682 if (asm.GetType (providerTypeName, false) != null)
688 if (GetTypeFromBin (providerTypeName) == null)
689 throw new HttpException (String.Format ("Profile provider type not found: {0}",
694 private bool CollectFiles (string dir, AppCodeAssembly aca)
696 bool haveFiles = false;
698 AppCodeAssembly curaca = aca;
699 foreach (string f in Directory.GetFiles (dir)) {
704 foreach (string d in Directory.GetDirectories (dir)) {
705 foreach (AppCodeAssembly a in assemblies)
706 if (a.SourcePath == d) {
710 if (CollectFiles (d, curaca))
717 private string BinDir {
721 AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
722 _bindir = Path.Combine (setup.ApplicationBase, setup.PrivateBinPath);