Imports System
Imports System.CodeDom
Imports System.Collections.Generic
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Web.Compilation
Imports System.Web.Hosting
Imports System.Xml
'Modified slightly from the c# at http://www.singular.co.nz/blog/archive/2008/02/03/build-providers-and-strongly-typed-page-urls-in-asp-net.aspx
Namespace StaticContent
'''
''' Generates a static class for urls within an ASP.NET project
'''
Public Class BuildProvider
Inherits System.Web.Compilation.BuildProvider
#Region "Fields"
Delegate Function Filter(ByVal info As FileSystemInfo, ByVal include As Regex, ByVal exclude As Regex) As Boolean
'''
''' Path to the web root for determining urls within the site
'''
Private _basePath As String
'''
''' Whether to convert urls to lowercase - based on your preference
'''
Private _useLowerCaseUrls As Boolean
'''
''' Whether to include the extension of the file in the name
''' of the member
'''
Private _includeExtension As Boolean
'''
''' Whether to remove the portion of the path skipped due to minimum depth constraints
''' of the member
'''
Private _truncatePathToStartingDepth As Boolean
'''
''' format string for resource value
'''
Private _resourceResolutionFormat As String
'''
''' Maximum recurring depth for the project - 1 for top level,
''' 2 for first level of subfolders etc...
'''
Private _maxDepth As Integer = 100
'''
''' Minimum recurring depth for the project - 1 for top level,
''' 2 for first level of subfolders etc...
''' the purpose it to not include files that are not deep enough in the tree to worrry about
'''
Private _minDepth As Integer
'''
''' Namespace the generated class will be placed within.
''' Leave empty to add to the global namesapce e.g. global::Href
'''
Private _namespace As String
'''
''' The name of the main class e.g. Href
'''
Private _className As String
'''
''' Current depth of recursion
'''
Private _depth As Integer
'''
''' Predicate to determine if a file should be included
'''
Private isValidFile As Filter
'''
''' Predicate to determine if a folder should be included
'''
Private isValidFolder As Filter
'rule to check for includes
Private Shared _includeRule As Regex
'rule to check for excludes
Private Shared _excludeRule As Regex
#End Region
#Region "GenerateCode"
'''
''' Initialises settings from the BuildProvider file, and
''' generates the appropriate code.
'''
'''
Public Overrides Sub GenerateCode(ByVal assemblyBuilder As AssemblyBuilder)
' init settings from xml source file
init()
' create root class e.g. global::Href
Dim root As CodeTypeDeclaration = createStaticClass(_className)
addSummary(root, "Provides access to urls within the project.")
Dim members As New Dictionary(Of String, Integer)()
build(New DirectoryInfo(_basePath), root, members)
Dim ns As New CodeNamespace()
ns.Types.Add(root)
' leave blank for global::namespace
If Not String.IsNullOrEmpty(_namespace) Then
ns.Name = _namespace
End If
Dim unit As New CodeCompileUnit()
unit.Namespaces.Add(ns)
assemblyBuilder.AddCodeCompileUnit(Me, unit)
End Sub
#End Region
#Region "build"
'''
''' Iterates over the given directory, adding the files as members to
''' the parent type, then recurses down the folder tree until the max
''' depth is reached.
'''
''' The directory to process
''' Parent class to add members too
Private Sub build(ByVal dir As DirectoryInfo, ByVal parent As CodeTypeDeclaration, ByVal members As Dictionary(Of String, Integer))
If _depth >= _maxDepth Then
Return
End If
_depth += 1
If _depth >= _minDepth Then
'members' keeps a record of the number of times a
'member name is repeated within the parent type/class.
'Pass this to 'ensureUniqueMemberName' to get the name
'with an index number appended to the end e.g. the second
'occurrence of "MyProperty" becomes "MyProperty1"
' process files:
' iterate over files and add the member
' public const string MyfileName = "~/myfileName.aspx";
'
Dim files As FileInfo() = dir.GetFiles()
For Each file As FileInfo In files
If isValidFile(file, _includeRule, _excludeRule) Then
Dim field As New CodeMemberField()
field.Name = getName(file)
ensureUniqueMemberName(members, field)
field.Type = New CodeTypeReference(GetType(String))
If String.IsNullOrEmpty(_resourceResolutionFormat) Then
field.Attributes = MemberAttributes.[Public] Or MemberAttributes.[Const]
field.InitExpression = getInitExpression(getUrl(file))
Else
field.Attributes = MemberAttributes.[Public] Or MemberAttributes.Static
field.InitExpression = New CodeSnippetExpression(String.Format(Globalization.CultureInfo.InvariantCulture, _resourceResolutionFormat, getUrl(file)))
End If
addSummary(field, getUrl(file, False))
parent.Members.Add(field)
End If
Next
End If
'
' process subfolders:
' iterate over folders and add a nested class
'
Dim subfolders As DirectoryInfo() = dir.GetDirectories()
For Each folder As DirectoryInfo In subfolders
If isValidFolder(folder, _includeRule, _excludeRule) Then
If _depth < _minDepth Then
build(folder, parent, members)
Else
Dim nested As CodeTypeDeclaration = createStaticClass(getName(folder))
addSummary(nested, "Provides access to urls under: {0}", getUrl(folder, False).TrimStart("~"c))
ensureUniqueMemberName(members, nested)
build(folder, nested, members)
' .ctor will have already been added to members
' so only add to parent if there are additional
' members present
If nested.Members.Count > 1 Then
parent.Members.Add(nested)
End If
End If
End If
Next
_depth -= 1
End Sub
#End Region
#Region "init"
'''
''' Initialises settings from the config file.
'''
'''
'''
''' Sample configuration - all nodes and elements are optional, but the file
''' needs a root node to load as an XmlDocument.
'''
'''
'''
'''
'''
'''
'''
'''
Private Sub init()
Try
_basePath = HostingEnvironment.ApplicationPhysicalPath
Dim xml As New XmlDocument()
Using stream As Stream = VirtualPathProvider.OpenFile(MyBase.VirtualPath)
xml.Load(stream)
End Using
' code below uses syntax, xml["elementName"] ?? xml.CreateElement( "elementName" );
' as a lazy initialisation technique, rather than checking for
' the node and performing alternate initialisation - this keeps
' it within the same reading context.
'
' general settings
'
Dim settings As XmlElement
If xml("settings") Is Nothing Then
settings = xml.CreateElement("settings")
Else
settings = xml("settings")
End If
_maxDepth = Integer.Parse(getAttributeValue(settings, "maxDepth", "100"), System.Globalization.CultureInfo.InvariantCulture)
_minDepth = Integer.Parse(getAttributeValue(settings, "minDepth", "0"), System.Globalization.CultureInfo.InvariantCulture)
_useLowerCaseUrls = Boolean.Parse(getAttributeValue(settings, "lowercaseUrls", "false"))
_includeExtension = Boolean.Parse(getAttributeValue(settings, "includeExtension", "false"))
_truncatePathToStartingDepth = Boolean.Parse(getAttributeValue(settings, "truncatePathToStartingDepth", "false"))
_resourceResolutionFormat = getAttributeValue(settings, "resourceResolutionFormat")
_namespace = getAttributeValue(settings, "namespace")
_className = getAttributeValue(settings, "className", "Href")
'
' files settings
'
Dim files As XmlElement = Nothing
If settings("files") Is Nothing Then
files = xml.CreateElement("files")
Else
files = settings("files")
End If
isValidFile = createFilter(getAttributeValue(files, "include", "\.aspx$"), getAttributeValue(files, "exclude"))
'
' folders settings
'
Dim folders As XmlElement = Nothing
If settings("folders") Is Nothing Then
folders = xml.CreateElement("folders")
Else
folders = settings("folders")
End If
isValidFolder = createFilter(getAttributeValue(folders, "include"), getAttributeValue(folders, "exclude", "App_|Bin"))
Catch ex As Exception
Console.WriteLine("Error:" + ex.ToString)
Throw
End Try
End Sub
#End Region
#Region "getAttributeValue"
'''
'''
'''
'''
'''
'''
'''
Private Shared Function getAttributeValue(ByVal parent As XmlElement, ByVal name As String, ByVal [default] As String) As String
Dim attribute As XmlAttribute = parent.Attributes.ItemOf(name)
If attribute Is Nothing Then
Return [default]
Else
Return attribute.Value.Trim()
End If
End Function
'''
'''
'''
'''
'''
'''
Private Shared Function getAttributeValue(ByVal parent As XmlElement, ByVal name As String) As String
Return getAttributeValue(parent, name, Nothing)
End Function
#End Region
#Region "createFilter"
'''
''' Creates a Predicate for filtering based on the given regex patterns
'''
'''
'''
'''
Private Shared Function createFilter(ByVal includePattern As String, ByVal excludePattern As String) As Filter
If Not String.IsNullOrEmpty(includePattern) Then
_includeRule = New Regex(includePattern, RegexOptions.Compiled Or RegexOptions.IgnoreCase)
End If
If Not String.IsNullOrEmpty(excludePattern) Then
_excludeRule = New Regex(excludePattern, RegexOptions.Compiled Or RegexOptions.IgnoreCase)
End If
Return createFilter(_includeRule, _excludeRule)
End Function
'''
''' Creates a Predicate for filtering based on the given regex
'''
'''
'''
'''
Private Shared Function createFilter(ByVal include As Regex, ByVal exclude As Regex) As Filter
If include IsNot Nothing AndAlso exclude IsNot Nothing Then
Return AddressOf ConvertedAnonymousMethod1
ElseIf include IsNot Nothing Then
Return AddressOf ConvertedAnonymousMethod3
ElseIf exclude IsNot Nothing Then
Return AddressOf ConvertedAnonymousMethod4
Else
Return AddressOf ConvertedAnonymousMethod2
End If
End Function
#End Region
#Region "getInitExpression"
'''
''' Returns the given value surrounded with quotes.
'''
'''
'''
Private Shared Function getInitExpression(ByVal value As String) As CodeSnippetExpression
Return New CodeSnippetExpression(String.Concat("""", value, """"))
End Function
#End Region
#Region "getName"
'''
''' Gets the normalised/escaped member name for the given file
'''
'''
'''
Private Function getName(ByVal file As FileSystemInfo) As String
Dim name As String = Path.GetFileNameWithoutExtension(file.Name)
If _includeExtension Then
name += file.Extension.Replace("."c, "_"c)
End If
name = Regex.Replace(name, "[^a-z0-9_]*", String.Empty, RegexOptions.Compiled Or RegexOptions.IgnoreCase)
' Ensure pascal casing of the name - not really required
'name = TextHelper.PascalCase( name );
' Escape names not starting with a letter
If Not Char.IsLetter(name(0)) Then
name = "_" + name
End If
Return name
End Function
#End Region
#Region "getUrl"
'''
''' Gets the url of the file, relative to the app root
'''
'''
'''
Private Function getUrl(ByVal file As FileSystemInfo) As String
Return getUrl(file, True)
End Function
'''
''' Gets the url of the file, relative to the app root
'''
'''
''' When true, allows lowercasing of the
''' url and stripping of default.aspx
'''
Private Function getUrl(ByVal file As FileSystemInfo, ByVal allowNormalisation As Boolean) As String
Dim url As String = file.FullName.Substring(_basePath.Length)
If allowNormalisation Then
If _useLowerCaseUrls Then
url = url.ToLowerInvariant
End If
If url.EndsWith("default.aspx", StringComparison.OrdinalIgnoreCase) Then
url = url.Substring(0, url.Length - 12)
End If
End If
'if this is not a directory and we're supposed to truncate paths after a certain depth, then do so
If _truncatePathToStartingDepth AndAlso Not System.IO.Directory.Exists(file.FullName) Then
If _minDepth > 0 Then
Dim newUrl As New Text.StringBuilder
Dim directoryPaths As String() = url.Split("\"c)
Dim max As Int32 = directoryPaths.Length - 1
For i As Int32 = _minDepth - 1 To max
newUrl.Append(directoryPaths(i))
If i < max Then
newUrl.Append("/")
End If
Next
Return newUrl.ToString
End If
Return url.Replace("\", "/")
Else
Return String.Concat("~/", url.Replace("\", "/"))
End If
End Function
#End Region
#Region "addSummary"
'''
''' Adds a summary doc comment to the type's comment collection
'''
'''
'''
'''
Private Shared Sub addSummary(ByVal type As CodeTypeMember, ByVal format As String, ByVal ParamArray args As Object())
addSummary(type.Comments, format, args)
End Sub
'''
''' Adds a summary doc comment to the collection
'''
'''
'''
'''
Private Shared Sub addSummary(ByVal comments As CodeCommentStatementCollection, ByVal format As String, ByVal ParamArray args As Object())
comments.Add(New CodeCommentStatement("", True))
comments.Add(New CodeCommentStatement(String.Format(Globalization.CultureInfo.InvariantCulture, format, args), True))
comments.Add(New CodeCommentStatement("", True))
End Sub
#End Region
#Region "createStaticClass"
'''
''' Creates a static class type with the given name
'''
'''
'''
Private Shared Function createStaticClass(ByVal name As String) As CodeTypeDeclaration
Dim type As New CodeTypeDeclaration(name)
type.TypeAttributes = type.TypeAttributes Or system.Reflection.TypeAttributes.Sealed
type.Attributes = MemberAttributes.[Public] Or MemberAttributes.[Static]
Dim ctor As New CodeConstructor()
ctor.Attributes = MemberAttributes.[Private]
type.Members.Add(ctor)
Return type
End Function
#End Region
#Region "ensureUniqueMemberName"
'''
''' Ensures a unique name within the dictionary to get the name
''' with an index number appended to the end e.g. the second
''' occurrence of "MyProperty" becomes "MyProperty1"
'''
''' A caveat of this method is where you may have two members like
''' MyPage.aspx, MyPage.html as well as MyPage1.aspx. A conflict will occur
''' giving duplicate members i.e. in the same order MyPage, MyPage1 and MyPage1
''' a second time. The likelihood of such conflicts is possible, with a possible
''' solution to recurse using ensureUniqueMemberName with the newly generated name,
''' or enumerate the parent type's Members to see whether there will be a conflict.
''' Dictionary containing the name of the members
''' in the current namespace as the key + the number of appearances as the value.
''' The next member to add to the current namespace.
Private Shared Sub ensureUniqueMemberName(ByVal members As Dictionary(Of String, Integer), ByVal type As CodeTypeMember)
Dim key As String = type.Name
Dim count As Integer
If members.TryGetValue(key, count) Then
type.Name = String.Concat(key, system.Threading.Interlocked.Increment(count))
members(key) = count
Else
members(key) = 0
End If
End Sub
Private Shared Function ConvertedAnonymousMethod1(ByVal info As FileSystemInfo, ByVal include As Regex, ByVal exclude As Regex) As Boolean
Return include.IsMatch(info.FullName) AndAlso Not exclude.IsMatch(info.FullName)
End Function
Private Shared Function ConvertedAnonymousMethod2(ByVal info As FileSystemInfo, ByVal include As Regex, ByVal exclude As Regex) As Boolean
Return True
End Function
Private Shared Function ConvertedAnonymousMethod3(ByVal info As FileSystemInfo, ByVal include As Regex, ByVal exclude As Regex) As Boolean
Return include.IsMatch(info.FullName)
End Function
Private Shared Function ConvertedAnonymousMethod4(ByVal info As FileSystemInfo, ByVal include As Regex, ByVal exclude As Regex) As Boolean
Return Not exclude.IsMatch(info.FullName)
End Function
#End Region
End Class
End Namespace