ILearnable .Net

November 22, 2010

WebActivator / PreApplicationStartMethod

Filed under: Uncategorized — andreakn @ 07:51

Yesterday I was totally taken aback by the tiny but excellent webactivator project. I totally had a near-magic experience, that’s when I’m almost starting to suspect something works because of tiny magic elves within the .Net runtime “just know what to do” and even though the code doesn’t seem like it’s doing the right things it’s still working.

Let me explain: WebActivator is something you will probably see more and more of the more you start to use modules from NuGet. It is a neat little dll which allows you to specify anywhere in your code that a certain method is to be called on application startup. See this example from Ninject, this class gets added to your solution when installing the ninject package with nuget:


[assembly: WebActivator.PreApplicationStartMethod(typeof(SinsenWeb.Web.AppStart_NinjectMVC3), "Start")]
namespace YourNameSpace {
    public static class AppStart_NinjectMVC3 {
        public static void RegisterServices(IKernel kernel) {
            //kernel.Bind<IThingRepository>().To<SqlThingRepository>();
        }

        public static void Start() {
            // Create Ninject DI Kernel
            IKernel kernel = new StandardKernel();

            // Register services with our Ninject DI Container
            RegisterServices(kernel);

            // Tell ASP.NET MVC 3 to use our Ninject DI Container
            DependencyResolver.SetResolver(new NinjectServiceLocator(kernel));
        }
    }

Ignore the ninject specific stuff. The goodness that webactivator brings to the table is the first line where a usage of the webactivator attribute will result in a certain method will be called on startup.

Interested in knowing just *how* WebActivator manages to do this I looked up the sourcecode. Latest version of  which is available here

as it only consists of two classes, the attribute which you can include (like ninject did) and the implementation of the logic I thought it would be easy to get a handle on this.

I’ll include the two sources here so you can see if you’re smarter than me in figuring out how this works.
Attribute:

using System;
using System.Reflection;

namespace WebActivator {
    // This attribute is similar to its System.Web namesake, except that
    // it can be used multiple times on an assembly.
    [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
    public sealed class PreApplicationStartMethodAttribute : Attribute {
        private Type _type;
        private string _methodName;

        public PreApplicationStartMethodAttribute(Type type, string methodName) {
            _type = type;
            _methodName = methodName;
        }

        public Type Type {
            get {
                return _type;
            }
        }

        public string MethodName {
            get {
                return _methodName;
            }
        }

        public void InvokeMethod() {
            // Get the method
            MethodInfo method = Type.GetMethod(
                MethodName,
                BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);

            if (method == null) {
                throw new ArgumentException(
                    String.Format("The type {0} doesn't have a static method named {1}",
                        Type, MethodName));
            }

            // Invoke it
            method.Invoke(null, null);
        }
    }
}

Implementation:


using System;
using System.IO;
using System.Reflection;
using System.Web;

namespace WebActivator {
    public class PreApplicationStartCode {
        public static void Start() {
            // Go through all the bin assemblies
            foreach (var assemblyFile in Directory.GetFiles(HttpRuntime.BinDirectory, "*.dll")) {
                var assembly = Assembly.LoadFrom(assemblyFile);

                // Go through all the PreApplicationStartMethodAttribute attributes
                // Note that this is *our* attribute, not the System.Web namesake
                foreach (PreApplicationStartMethodAttribute preStartAttrib in assembly.GetCustomAttributes(
                    typeof(PreApplicationStartMethodAttribute),
                    inherit: false)) {

                    // Invoke the method that the attribute points to
                    preStartAttrib.InvokeMethod();
                }
            }
        }
    }
}

Hmm, so basically, we declare an attribute and we have a static method which when run will track down all instances of this attribute in all dlls in the bin folder and fire the method as specified in each of those instances.

But *who* calls the static method?

I had a look in the Properties\AssemblyInfo.cs file which looks like this:

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Web;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("WebActivator")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Microsoft")]
[assembly: AssemblyProduct("WebActivator")]
[assembly: AssemblyCopyright("Copyright © Microsoft 2010")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components.  If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("3bc078bd-ade4-4271-964f-1d041508c419")]

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

[assembly: PreApplicationStartMethod(typeof(WebActivator.PreApplicationStartCode), "Start")]

and in the bottom of that file I finally started to believe that magic little elves live within the jit-compiler and make everything fine and dandy even though it seems like the feature is using itself to start itself.

Maybe there really *are* turtles all the way down?

Have you spotted how it works yet?

I’ll give you some whitespace to think of the solution before I blurt it out and ruin your fun

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


WebActivator.PreApplicationStartMethod != System.Web.PreApplicationStartMethod

WebActivator doesn’t use itself but rather a feature of the Asp.Net 4.0 runtime to start itself off. The whole motivation for WebActivator is that the feature built into Asp.Net 4.0 has a design flaw in that it only allows a single instance of the attribute within each dll, making it hard to use for NuGet scenarios where you would want to use multiple different packages each with their own startup logic independent of eachother, you would have to manually merge the logic into one place if you wanted to use the Asp.Net 4.0 implementation.

However, since WebActivator.dll is a tiny separate dll it can make use of the asp.net feature to kickstart the other dlls.

I think that I would prefer if the AssemblyInfo.cs file was a little more specific in its usage of the asp.net feature, seeing that it’s basically reimplementing it itself. Instead of relying on a using statement for System.Web, I would prefer if the attribute was declared as

[assembly: System.Web.PreApplicationStartMethod(typeof(WebActivator.PreApplicationStartCode), "Start")]

 

October 12, 2010

How to make your world better one browser extension at a time

Filed under: Uncategorized — andreakn @ 21:22

As a consultant I have been exposed to a lot of different timesheet applications. Everything from a simple excel sheet to fairly advanced web sites into which you enter the amount of time spent on various (hopefully) project-related activities. The fidelity with which the hours are to be registered also varies wildly. Generally being a bit of a slob I prefer when the input process is simple and the fidelity is almost nonexisten. This frees me up to deliver quality work instead of painstakingly documenting *what* work I did *when* and for *how long*.

In my simplistic world-view the hourly report forms you have to fill out are a necessary evil, but they’re still evil :)

So, when I got subcontracted through a firm with this input process: (snip: http://screencast.com/t/mZ51HlB8B )
First I threw a fit and then I started to think: “is there any way for me to automate this”.

as it turns out, yes there is. I won’t bore you with all the juicy details on how to create a chrome plugin that modifies a webpage on your behalf,
but in essence all you do is to have a manifest file declare on which urls this plugin should kick in, and what javascript files it should inject at the end of the loading process for that page.

For me in this case the file (which must be called manifest.json by the way) looks like this:

{
  "name" : "ElanIt Timereg decrapifier",
  "version" : "0.1",
  "description" : "Makes timereg useable",

  "content_scripts": [
    {
      "matches": ["https://timereg.elanit.no/Default.aspx*"],
      "js": ["jquery.js", "timeregmod.js"]
    }
  ]
}

which as you can see loads a bundled version of a minified jquery 1.4.2 and also a custom js file which contains the logic I needed to inject onto the page.
here is the timeregmod.js file:



$.fn.getNonColSpanIndex = function () {
    if (!$(this).is('td') && !$(this).is('th'))
        return -1;

    var allCells = this.parent('tr').children();
    var normalIndex = allCells.index(this);
    var nonColSpanIndex = 0;

    allCells.each(
        function (i, item) {
            if (i == normalIndex)
                return false;

            var colspan = $(this).attr('colspan');
            colspan = colspan ? parseInt(colspan) : 1;
            nonColSpanIndex += colspan;
        }
    );

    return nonColSpanIndex;
};

$(document).ready(function () {
    findAllWorkDays($('.ITMTimesheetDetail')).not(':last').each(function () {
        var tr = $(this);
        tr.append($('<td></td>').append($('<input type="button"></input>)').val('>>').css('font-size','9px').click(function () {
            var input = $(this);
            var thisRow = input.parents('tr').first();

            var nextRow = findNextWorkDay(thisRow);

            $('input[type=text]', thisRow).each(function () {
                var textbox = $(this);
                var index = textbox.parent().getNonColSpanIndex();

                $('td:eq(' + index + ') input', nextRow).val(textbox.val());
            });
        })));
    });
    $('.ITMTimesheetDetail tr:eq(2)').each(function () {
        var tr = $(this);
        tr.append($('<td></td>').append($('<input type="button"></input>)').val('Kopier til alle').css('font-size', '9px').click(function () {
            $('input[type=text]', tr).each(function () {
                var textbox = $(this);
                var value = textbox.val();
                var index = textbox.parent().getNonColSpanIndex();
                var workdays = findAllWorkDays('.ITMTimesheetDetail');
                $('td:eq(' + index + ') input', workdays).val(value);
            });
        })));
    });
});

function findAllWorkDays(table) {
    return $('tr:gt(1)', table).filter(function (index) {
        var tr = $(this);
        return isWorkDayRow(tr);
    });
}

function isWorkDayRow(tr){
    if(tr.children().first().hasClass('ITMTimesheetDetailDayOfMonthHoliday') || tr.children().first().hasClass('ITMTimesheetDetailDayOfMonthSaturday')){
        return false;
    }
    return true;
}

function findNextWorkDay(currentRow) {
   var nextRow = currentRow.next();

   while (!isWorkDayRow(nextRow)) {
       nextRow = nextRow.next();
   }
   return nextRow;
}



I won’t pretend that I’m overly happy with the javascript, it should be shorter and better, but it gets the job done, so I’m stopping working on it.

The timesheet application (which is called Manpower Timereg apparently) now looks like this for me (when browsing using google chrome with the plugin loaded):
http://screencast.com/t/cpBtfwBe7t7

(yes, the buttons on the right hand side was added by this plugin).

SO: if you have to deal with this (dare I say somewhat challenging) system and would like to improve your own experience, feel free to take the sourcecode in this blogpost and create your own chrome plugin. You probably want to change the url in manifest.json (unless you happen to be subcontracting for ElanIt in norway)

creating a chrome plugin based on this is as simple as:
- create a new folder and call it MySpecialPlugin or something like that
- download the latest version of jquery into that folder and rename the file to jquery.js
- put the two files as described in this blogpost into the folder.
- in chrome: navigate to the url chrome://extensions and click the “load unpacked extension”
- select the folder you created a few bullets ago

and voila: Bob’s your uncle

September 20, 2010

Windows authentication nightmare gone bad…. with a vengeance…

Filed under: Uncategorized — andreakn @ 21:11
Tags: , , ,

Today I spent a large portion of my day chasing around bugs related to windows authentication and episerver.

My basic premise was this:
- End users will connect to the site I’m making use IE7 under “local intranet” thereby magically getting access without logging in. (WindowsRoleProvider)
- developers / support personnel / admins will use EPiServer logins (SqlRoleProvider)
- in order to get this to work I need to use the MultiplexingRoleProvider.

There are numerous writeups on how to use the multiplexingroleprovider, but none actually say out loud *you cannot use MultiplexingRoleProvider and ALSO get automatic NTLM windows authentication* So now I’ve said it.

The first hint I got was this image from IIS when I tried to set it up: http://screencast.com/t/NjJiNzE0MDE where IIS7 complained that you cannot have your cake and eat it too (you can’t use both challenge-based (NTLM) and redirect-based (Forms) auth in the same app.

I talked to a few guys I trust and they convinced me that “it should be possible, I think I have done it at some point but I can’t remember how”. Time would convince me otherwise, though.

Setting up Multiplexer is easy, just look here: http://sdk.episerver.com/library/CMS5/Developers%20Guide/Membership%20and%20Role%20Providers.htm?id=24871
I always wondered what the “BUILTIN\” signifies in WindowsMembershipProvider, for the record it is there to nullify the prefix of the local groups on the machine (Administrators and Everyone) which are actually called BUILTIN\Administratos and BUILTIN\Everyone when under UAC

I was able to set up the MPRP easily enough, but the IE auto-login didn’t work.

I was able to get IE auto-login to work, but only using windows authentication only.
Also I wasn’t able to log in to localhost for some reason. but when I accessed the server from a different machine I could authenticate.

So then I got carried away on a wild goose chase (maybe?) with there might be problems using windows auth on localhost: http://support.microsoft.com/kb/896861/en-us So I regedited the server a few times and reset it a few times, cursed the sky a few times, fetal-cried a few brave tears, then I pulled myself together and went for one final google-push.

And lo and behold: http://www.andornot.com/blog/post/How-to-solve-4012-errors-related-to-Windows-Authentication-and-the-BUILTIN5cAdministrators-role-in-IIS-7.aspx

Turns out that when UAC is turned on on a Windows Server 2008, the BUILTIN\Administrators group is banned from authenticating, but only when the request is made from the local machine. (how weird is that? probably some security scenario I’m too tired to consider) So the final solution was to greate a new local group and assign appropriate God-like priviliges to that group, (I considered calling it Developersdevelopersdevelopers) and put all the devs into that group and configure the site to accept users belonging to devdevdev. So that we can deploy the site (admins only.. UAC is your friend here to help you) and also test that the deploy went well, all on the same machine.

PROGRESS!

August 31, 2010

Upgrading EPiServer to CMS6

Filed under: Uncategorized — andreakn @ 06:52
Tags: ,

Had this problem:

Cannot resolve dependencies for the following module(s)
EPiServer.Cms.Shell.UI.InitializableModule
EPiServer.Cms.Shell.UI.InitializableModule
EPiServer.Web.InitializationModule
EPiServer.Web.InitializationModule

Turns out the C:\Program Files\EPiServer\CMS\6.0.530.0\bin doesn’t contain all the necessary dlls.

instead you can either do a clean install and steal the dlls from that site’s /bin or you can fetch them from C:\Program Files\EPiServer\CMS\6.0.530.0\VSTemplates\EPiServerProject.zip

(thanks Jamie Dixon: http://www.jamie-dixon.co.uk/episerver/upgrading-episerver-to-version-6/ )

EPiServer site error: CS1519: Invalid token ‘,’ in class, struct, or interface member declaration

Filed under: Uncategorized — andreakn @ 06:46
Tags:

After being bitten by this for the N’th time I’m going to write down the solution this time:

EPiServer dlls are missing from /bin

There, I said it. Now I don’t need to research this the next time it happens.

August 27, 2010

Running two versions of NHibernate side by side

Filed under: Uncategorized — andreakn @ 08:33
Tags: , , ,

In my current project we had used NHibernate/Fluent NHibernate (version 2.1.2.4000) for some custom data access for data which shouldn’t live in the EPiServer DB.

Then suddenly we started to integrate EPiServer Community 3.2 (EPiServer Relate + v1.0.1.0) into our solution and stumbled a bit on the fact that it used a different version of NHibernate (version 1.2.0.4000 to be exact)

on the verge of giving up and rewriting our custom data access to raw ADO.net I stumbled upon the solution: using assemblyBinding to instruct .Net *where* to look for the different versions of the dlls. Here’s a snippet from web.config:

<configuration>
   ...
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Iesi.Collections" publicKeyToken="aa95f207798dfdb4" culture="neutral" />
        <codeBase version="1.0.0.3" href="dlllib\nhibernate1.2\Iesi.Collections.dll" />
        <codeBase version="2.1.2.4000" href="dlllib\nhibernate2.1\Iesi.Collections.dll" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="NHibernate" publicKeyToken="aa95f207798dfdb4" culture="neutral" />
        <codeBase version="1.2.0.4000" href="dlllib\nhibernate1.2\NHibernate.dll" />
        <codeBase version="2.1.2.4000" href="dlllib\nhibernate2.1\NHibernate.dll" />
      </dependentAssembly>
      ...
     </assemblyBinding>
  </runtime>
</configuration>

So what I did was to have a folder structure like this

- site
- – bin
- – dlllib
- – - nhibernate1.2
- – - – Nhibernate.dll (v1.2.0.4000)
- – - – Iesi.Collections.dll (v1.0.0.3)
- – - nhibernate2.1
- – - – Nhibernate.dll (v2.1.2.4000)
- – - – Iesi.Collections.dll (v2.1.2.4000)

So there it is, no more headscratching when different versions of the same dlls are needed. “codeBase” to the rescue!

August 24, 2010

Polymorphic web services (asmx)

Filed under: Uncategorized — andreakn @ 12:43

I had the “pleasure” of creating a dummy web service having only the required interface to work with. and discovered that the service had a method which took polymorphic data.

After spending a bit of time poking at it with a stick I realized that the magic trick was to tag the service methods with XmlInclude attributes for each of the child types that could be sent over.

Also I realized that I’m not a huge fan of polymorphic web service methods.

August 6, 2010

rework of strongly typed access to language files in EPiServer

Filed under: Uncategorized — andreakn @ 12:34

I showed my strongly typed language handling in EPiServer to a guy at work who immediately pointed out a weakness: ReSharper will harass you and try to get you to add using statements all over the code, which in turn would reduce readability of the language strings.

This is because the hierarchical information of the language files was encoded into namespaces. I remodelled it to use nested public static classes instead and it works just the same, here’s the code:

<#@ template debug="true" hostSpecific="true" #><#@ assembly name="System.Xml"#><#@ assembly name="System.Xml.Linq" #><#@ assembly name="System.Core" #><#@ import namespace="System.Xml.Linq"#><#@ import namespace="System.Linq"#><#@ import namespace="System.Collections.Generic"#><#
int skipLevels = 1; //depends on xml file, for standard episerver use 2
string xmlFilePath = @"\lang.xml";
string baseCodeGenNamespace = "Lang";
XDocument xdoc = XDocument.Load(System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+xmlFilePath);
GenerateLanguageAccessors(xdoc.Root, baseCodeGenNamespace,skipLevels);
#><#+
   void GenerateLanguageAccessors(XNode xNode, string baseNS, int skipLevels)
        {
            this.WriteLine("public static class " + baseNS + "{");
            GenerateLanguageAccessorsRecursive(xNode, new List<string>(), skipLevels, new List<string>());
            this.WriteLine("}");
           
        }

         void GenerateLanguageAccessorsRecursive(XNode xNode, List<string> ns, int skipLevels, List<string> cache)
        {
            string indenting = Times("\t", ns.Count - skipLevels);

            if (xNode is XText)
            {
                if (ns.Count < skipLevels) return;

                string theKey = "/" + string.Join("/", ns.Skip(skipLevels+1).ToArray());
                string s = indenting + "public static string Text{ get{ return Utils.LanguageUtil.Translate(\"<KEY>\");} }\n";
                s += indenting + "public static string TextOrDefault(string defaultString){ return Utils.LanguageUtil.TranslateOrDefault(\"<KEY>\",defaultString);}";
                s = s.Replace("<KEY>", theKey);
                this.WriteLine(s);
                
                return;
            }
            if (xNode is XElement)
            {
                var xElement = xNode as XElement;
                var newNameSpace = ns.ToArray().ToList();
                newNameSpace.Add(xElement.Name.ToString());
                var pathKey = string.Join(".", newNameSpace);
                var shouldWriteClass = (ns.Count > skipLevels && !cache.Contains(pathKey));
                if (shouldWriteClass) this.WriteLine(indenting + "public static class " + newNameSpace.Last() + "{");
                cache.Add(pathKey);
                foreach (var subNode in xElement.Nodes())
                {
                    GenerateLanguageAccessorsRecursive(subNode, newNameSpace, skipLevels, cache);
                }
                if (shouldWriteClass) this.WriteLine(indenting + "}");
            }
        }

         string Times(string input, int times)
        {
            string ret = "";
            for(int i = 0; i<times; i++)
            {
                ret += input;
            }
            return ret;
        }
#>

the reason everything is squashed together in the top is to avoid getting a lot of linebreaks in the start of the file.

Using nested classes comes with two small limitations: you cannot have the language string contain two identical names in a row (for example: “/strings/pages/frontpage/frontpage/heading”) and you should never have it end with Text:
(“/strings/pages/frontpage/heading/Text”)

(the C# compiler refuses to compile the generated code in both these two cases with the error “member names cannot be the same as their enclosing type”)

August 2, 2010

Using T4 to manage config files for complex deployment scenarios

Filed under: Uncategorized — andreakn @ 06:48
Tags: , ,

Using T4 to generate multiple versions of xml config files with inheritance

(NB: baked into this sample is a method for generating multiple output files from T4, it is stolen shamelessly from Damien Guard (thanks Damien!), except I couldn’t get his deleting of files no longer needed to work (it deleted needed files as well) so I commented out that bit and it worked like a charm.
For a full description of that part of the script go to http://damieng.com/blog/2009/11/06/multiple-outputs-from-t4-made-easy-revisited and read Damiens excellent blog)

quite a while back I got really irritated with how .Net does configuration files. My main gripe is that there is no easy way to
relate the configuration for the local dev environment, build server, test server(s), staging server(s) and production server(s).

let me give you an example: In a standard Web Application Project the web.config file is a single file which is used to define all the configuration
data for the entire site:
- WCF web services exposed
- WCF web services used
- logging
- App settings
- etc.

Now i do know that you can split the file up where each section can have its own file, and use different files for the different environments,
but that doesn’t really solve the problem as
- you end up with HEAPS of files if you want fine grained control
- you cannot really use that strategy for having differences within a section (appSettings for instance)
- you need to do a lot of file copying magic in order to get the correct sections together for a workable web.config for a given environment
- even if you only need one tiny difference between say testserver1 and testserver2 the entire section must be duplicated into two different files
(major DRY violation)

I have spent a lot of time pondering how to do this better (WAY too much time actually, but then I’m not really that smart :) ) and have come up with
a way to define configuration files using a sort of inheritance scheme. That way you can have a setup like this:
- web.config.root ( – file that defines 90% of the configuration, everything that is common throughout all environments)
- web.config.build ( – contains specifics for the build server )
- web.config.dev ( – contains common specifics for local dev instances )
- web.config.dev.local ( one per dev, not checked into SCM – contains specifics for each dev, local testing ground)
- web.config.test ( – defines common specifics for all test servers )
- web.config.test.testserver1 ( – defines specifics for a given test machine )
- etc.

and then have some engine process these source files in a controlled order. For instance:
- local web.config (used for local development) = web.config.root + web.config.dev + web.config.dev.local
- buildserver web.config = web.config.root + web.config.build
- testserver1 web.config = web.config.root + web.config.test + web.config.test.testserver1

so that for the testserver1 config file the web.config.root would first be applied, then settings in web.config.test would be applied, adding to
and overriding the settings from the root file. and lastly the settings in web.config.test.testserver1 would be applied, adding to and overriding
the settings from the resulting merge between root and test. (phew!)

There are two main problems with such a setup that must be solved:
A) how to identify which setting to override when there are multiple possibilities
B) how to define a setting in a base config file, then delete that setting in an overriding config file

for solving A) I have two rules:
- if the element is an only child (or at least the only child with that element name) then that element can be
overridden / expanded by subsequent input files.
- if an element has siblings with the same element name then it must be identified by an identificator attribute.
For standard .net configuration they are (in prioritized order) “id”, “name”, “key”, “path”.
- if there are equal siblings without any of these identificators the whole process throws up and informs that an ambiguity exists.

for solving B) I introduce the magic keyword DELETEME. I’ll give two examples:

1)

<configuration>
	<location path="Documents" DELETEME="true" />
</configuration>

2)

<configuration>
	<system.web>
      <httpRuntime maxRequestLength="DELETEME" />
	</system.web>
</configuration>

the config in 1) will delete the entire configuration for location path=”documents”
the config in 2) will delete the attribute maxRequestLength but otherwise leave the httpRuntime element alone

so, on to the code. To get the files generated automatically I’m using T4. I use two files, the first is a .ttinclude file which defines both the
xml merging logic and logic for generating multiple output files:

first, here’s the ConfigurationMerger.ttinclude file


<#@ assembly name="System.Core"#>
<#@ assembly name="System.Data.Linq" #>
<#@ assembly name="EnvDTE"#>
<#@ assembly name="System.Xml"#>
<#@ assembly name="System.Xml.Linq"#>
<#@ import namespace="System"#>
<#@ import namespace="System.CodeDom"#>
<#@ import namespace="System.CodeDom.Compiler"#>
<#@ import namespace="System.Collections.Generic"#>
<#@ import namespace="System.Data.Linq"#>
<#@ import namespace="System.Data.Linq.Mapping"#>
<#@ import namespace="System.IO"#>
<#@ import namespace="System.Linq"#>
<#@ import namespace="System.Reflection"#>
<#@ import namespace="System.Text"#>
<#@ import namespace="System.Xml.Linq"#>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating"#>

<#+ 
   
Dictionary<string, string> MergeConfigFiles(string inputFilesFolder, string outputFilesFolder, Dictionary<string, string[]> setup){
	var ret = new Dictionary<string, string>();
	var merger = new XmlConfigMerge("name", "id", "key");
   Dictionary<string, string> configFiles = new Dictionary<string, string>();
    DirectoryInfo di = new DirectoryInfo(System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+inputFilesFolder);
	
    foreach(var f in di.GetFiles())
    {
        configFiles[f.Name] =File.ReadAllText(f.FullName);
    }

    foreach (var outputFileName in setup.Keys)
    {
        List<XDocument> xdocs = new List<XDocument>();
        foreach (var inputFileName in setup[outputFileName])
        {
            if (configFiles.ContainsKey(inputFileName))
            {
                xdocs.Add(XDocument.Parse(configFiles[inputFileName]));
            }
        }
        string result = merger.MergeConfigs(xdocs[0], xdocs.Skip(1).ToList()).ToString();
     	ret[outputFilesFolder+outputFileName] = result;
    }
	return ret;
}
	

public class XmlConfigMerge
    {
        public string DeleteKeyword = "DELETEME";

        public XmlConfigMerge(params string[] unique)
        {
            UniqueAttributes = unique.ToList();
        }

        public List<string> UniqueAttributes { get; set; }

        private XElement FindByAttr(IEnumerable<XElement> elements, string attribute, string value)
        {
            return elements.FirstOrDefault(e => HasAttribute(e, attribute, value));
        }

        private bool HasAttribute(XElement elem, string attributeName)
        {
            var attr = elem.Attribute(attributeName);
            if (attr != null
                && !string.IsNullOrEmpty(attr.Value)
                )
            {
                return true;
            }
            return false;
        }

        private bool HasAttribute(XElement elem, string attributeName, string attributeValue)
        {
            if (HasAttribute(elem, attributeName))
            {
                if (0 ==
                    string.Compare(elem.Attribute(attributeName).Value, attributeValue,
                                   StringComparison.InvariantCultureIgnoreCase))
                {
                    return true;
                }
            }
            return false;
        }


        public XDocument MergeConfigs(XDocument template, List<XDocument> dataFiles)
        {
            if (dataFiles == null || dataFiles.Count == 0) return template;
            var temp = template;
            foreach (var data in dataFiles)
            {
                temp = MergeConfigs(temp, data);
            }
            return temp;
        }

        private XDocument MergeConfigs(XDocument template, XDocument data)
        {
            var rootNodeTemplate = template.Root;
            var rootNodeData = data.Root;
            MergeConfigs(rootNodeTemplate, rootNodeData);
            return template;
        }

        private void MergeConfigs(XElement template, XElement data)
        {
            bool shouldDeleteNode = ShouldDeleteNode(data);
            if (shouldDeleteNode)
            {
                template.Remove();
                return;
            }

            MergeAttributes(template, data);

            if (!data.HasElements)
            {
                if (data.Value == DeleteKeyword) template.Value = string.Empty;
                else if (!string.IsNullOrEmpty(data.Value)) template.Value = data.Value;
                return;
            }


            var templateElements = template.Elements();
            var dataElements = data.Elements();


            foreach (var dataNode in dataElements)
            {
                KeyValue uniquey = GetUniqueKey(dataNode);

                var matchingNodes = templateElements.Where(x => x.Name == dataNode.Name);
                if (matchingNodes.Count() == 0)
                {
                    template.Add(dataNode);
                    continue;
                }

                if (uniquey == null && matchingNodes.Count() > 1)
                {
                    throw new ApplicationException("Cannot merge into file containing multiple undelimited equal siblings, needs attribute with name: " + string.Join(", ", UniqueAttributes.ToArray()));
                }
                if (uniquey == null && matchingNodes.Count() == 1)
                {
                    var templateNode = templateElements.Where(x => x.Name == dataNode.Name).First();
                    MergeConfigs(templateNode, dataNode);
                    continue;
                }

                if (uniquey != null)
                {
                    var ambigous =
                        templateElements.Count(
                            x =>
                            x.Attribute(uniquey.Key) != null &&
                            x.Attribute(uniquey.Key).Value.ToLower() == uniquey.Value.ToLower()) > 1;
                    if (ambigous)
                    {
                        throw new ApplicationException("Cannot merge into file containing multiple equal siblings with same identifier, needs unique value for attribute with name: " + string.Join(", ", UniqueAttributes.ToArray()));
                    }
                    var templateNode = (from x in templateElements
                                        where
                                            x.Attribute(uniquey.Key) != null
                                            && x.Attribute(uniquey.Key).Value.ToLower() == uniquey.Value.ToLower()
                                        select x).FirstOrDefault();
                    if (templateNode == null)
                    {
                        template.Add(dataNode);
                        continue;
                    }
                    else
                    {
                        MergeConfigs(templateNode, dataNode);
                        continue;
                    }
                }

            }
            return;

        }

        private void MergeAttributes(XElement template, XElement data)
        {
            foreach (var attrib in data.Attributes())
            {
                var value = attrib.Value;
                if (value == DeleteKeyword)
                {
                    value = null;
                }
                template.SetAttributeValue(attrib.Name, value);
            }
        }

        private KeyValue GetUniqueKey(XElement element)
        {
            foreach (var key in UniqueAttributes)
            {
                var elementsUnique = element.Attributes(key);
                if (elementsUnique.Count() == 1)
                {
                    return new KeyValue { Key = key, Value = elementsUnique.First().Value };
                }
            }
            return null;
        }

        private bool ShouldDeleteNode(XElement data)
        {
            var deleteattrib = data.Attributes(DeleteKeyword);
            if (deleteattrib.Count() == 1 && bool.Parse(deleteattrib.First().Value))
            {
                return true;
            }
            return false;
        }
    }

    public class KeyValue
    {
        public string Key { get; set; }
        public string Value { get; set; }
    }
	
	
// CodegenManager class records the various blocks so it can split them up
class CodegenManager {
    private class Block {
        public String Name;
        public int Start, Length;
    }

    private Block currentBlock;
    private List<Block> files = new List<Block>();
    private Block footer = new Block();
    private Block header = new Block();
    private ITextTemplatingEngineHost host;
    private StringBuilder template;
    protected List<String> generatedFileNames = new List<String>();

    public static CodegenManager Create(ITextTemplatingEngineHost host, StringBuilder template) {
        return (host is IServiceProvider) ? new VSCodegenManager(host, template) : new CodegenManager(host, template);
    }

    public void StartNewFile(String name) {
        if (name == null)
            throw new ArgumentNullException("name");
        CurrentBlock = new Block { Name = name };
    }

    public void StartFooter() {
        CurrentBlock = footer;
    }

    public void StartHeader() {
        CurrentBlock = header;
    }

    public void EndBlock() {
        if (CurrentBlock == null)
            return;
        CurrentBlock.Length = template.Length - CurrentBlock.Start;
        if (CurrentBlock != header && CurrentBlock != footer)
            files.Add(CurrentBlock);
        currentBlock = null;
    }

    public virtual void Process(bool split) {
        if (split) {
            EndBlock();
            String headerText = template.ToString(header.Start, header.Length);
            String footerText = template.ToString(footer.Start, footer.Length);
            String outputPath = Path.GetDirectoryName(host.TemplateFile);
            files.Reverse();
            foreach(Block block in files) {
                String fileName = Path.Combine(outputPath, block.Name);
                String content = headerText + template.ToString(block.Start, block.Length) + footerText;
                generatedFileNames.Add(fileName);
                CreateFile(fileName, content);
                template.Remove(block.Start, block.Length);
            }
        }
    }

    protected virtual void CreateFile(String fileName, String content) {
        if (IsFileContentDifferent(fileName, content))
            File.WriteAllText(fileName, content);
    }

    public virtual String GetCustomToolNamespace(String fileName) {
        return null;
    }

    public virtual String DefaultProjectNamespace {
        get { return null; }
    }

    protected bool IsFileContentDifferent(String fileName, String newContent) {
        return !(File.Exists(fileName) && File.ReadAllText(fileName) == newContent);
    }

    private CodegenManager(ITextTemplatingEngineHost host, StringBuilder template) {
        this.host = host;
        this.template = template;
    }

    private Block CurrentBlock {
        get { return currentBlock; }
        set {
            if (CurrentBlock != null)
                EndBlock();
            if (value != null)
                value.Start = template.Length;
            currentBlock = value;
        }
    }

    private class VSCodegenManager: CodegenManager {
        private EnvDTE.ProjectItem templateProjectItem;
        private EnvDTE.DTE dte;
        private Action<String> checkOutAction;
        private Action<IEnumerable<String>> projectSyncAction;

        public override String DefaultProjectNamespace {
            get {
                return templateProjectItem.ContainingProject.Properties.Item("DefaultNamespace").Value.ToString();
            }
        }

        public override String GetCustomToolNamespace(string fileName) {
            return dte.Solution.FindProjectItem(fileName).Properties.Item("CustomToolNamespace").Value.ToString();
        }

        public override void Process(bool split) {
            if (templateProjectItem.ProjectItems == null)
                return;
            base.Process(split);
            projectSyncAction.EndInvoke(projectSyncAction.BeginInvoke(generatedFileNames, null, null));
        }

        protected override void CreateFile(String fileName, String content) {
            if (IsFileContentDifferent(fileName, content)) {
                CheckoutFileIfRequired(fileName);
                File.WriteAllText(fileName, content);
            }
        }

        internal VSCodegenManager(ITextTemplatingEngineHost host, StringBuilder template)
            : base(host, template) {
            var hostServiceProvider = (IServiceProvider) host;
            if (hostServiceProvider == null)
                throw new ArgumentNullException("Could not obtain IServiceProvider");
            dte = (EnvDTE.DTE) hostServiceProvider.GetService(typeof(EnvDTE.DTE));
            if (dte == null)
                throw new ArgumentNullException("Could not obtain DTE from host");
            templateProjectItem = dte.Solution.FindProjectItem(host.TemplateFile);
            checkOutAction = (String fileName) => dte.SourceControl.CheckOutItem(fileName);
            projectSyncAction = (IEnumerable<String> keepFileNames) => ProjectSync(templateProjectItem, keepFileNames);
        }

        private static void ProjectSync(EnvDTE.ProjectItem templateProjectItem, IEnumerable<String> keepFileNames) {
            var keepFileNameSet = new HashSet<String>(keepFileNames);
            var projectFiles = new Dictionary<String, EnvDTE.ProjectItem>();
            var originalFilePrefix = Path.GetFileNameWithoutExtension(templateProjectItem.get_FileNames(0)) + ".";
            foreach(EnvDTE.ProjectItem projectItem in templateProjectItem.ProjectItems)
                projectFiles[projectItem.get_FileNames(0)] = projectItem;

            // Remove unused items from the project
			foreach(var pair in projectFiles)
                if (!keepFileNames.Contains(pair.Key) && !(Path.GetFileNameWithoutExtension(pair.Key) + ".").StartsWith(originalFilePrefix))
                    //pair.Value.Delete();

            // Add missing files to the project
            foreach(String fileName in keepFileNameSet)
                if (!projectFiles.ContainsKey(fileName))
                    templateProjectItem.ProjectItems.AddFromFile(fileName);
        }

        private void CheckoutFileIfRequired(String fileName) {
            var sc = dte.SourceControl;
            if (sc != null && sc.IsItemUnderSCC(fileName) && !sc.IsItemCheckedOut(fileName))
                checkOutAction.EndInvoke(checkOutAction.BeginInvoke(fileName, null, null));
        }
    }
}
#>


This file can be used as is with no modifications. Just put it into the root directory of the project

and then there is the actual T4 file which will cause stuff to be generated: configuration.tt
(you can call it anything you like)

<#@ template debug="true" hostSpecific="true" #>
<#@ output extension=".log" #>
<#@ include file="ConfigurationMerger.ttinclude"  #>
<#   
var setup = new Dictionary<string, string[]>();
string inputFilesRelativeRootPath = @"\";
string outputFilesRelativeRootPath = @"\";
//////////////////////////////////////////////////////////////	
//Configure the configuration generation process like this: 
//setup[<outputfilename>] = new []{<input1>,<input2>,<input3>,..};
//
// configs are merged in the order given (input 3 overrides input 2, which overrides input1)
//
// make your modifications below here
//////////////////////////////////////////////////////////////
inputFilesRelativeRootPath = @"\config\input";
setup[@"web.config"] = new []{"web.config.template","web.config.dev","web.config.dev.local"};
setup[@"config\output\web.config.test1"] = new []{"web.config.template","web.config.test","web.config.test.testserver1"};
setup[@"config\output\web.config.test2"] = new []{"web.config.template","web.config.test","web.config.test.testserver2"};
setup[@"config\output\web.config.build"] = new []{"web.config.template","web.config.build"};







////////////////////////////////////////////////////////////////
//End of custom configuration, the rest is just static stuff:
////////////////////////////////////////////////////////////////


var configs = MergeConfigFiles(inputFilesRelativeRootPath, outputFilesRelativeRootPath, setup);
var manager = CodegenManager.Create(Host, GenerationEnvironment);
foreach(var filepath in configs.Keys){
	#> 
	-------------- <#= System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+filepath #> --------
	<#= configs[filepath] #>
	------------------------------------------------------------
	<#
	 manager.StartNewFile(System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+filepath);
	#><#= configs[filepath] #><#
	manager.EndBlock();
}
manager.Process(true);
#>

This file should be tweaked to fit your project (you only need to tweak within the top part as indicated).
For instance: in this example the output files are all placed in the same directory with
different file endings, but that can be customized any way you like. (for instance having all the files called web.config but be in separate directories)

In addition to the output files, a separate log file is written called Configuration.log. This file contains all the resulting config files for easy
reference.

Normally the code(config) generation will only be triggered by a modification to the .tt file, but there is an easy way to get T4 to fire on every build.

I would suggest not checking the generated files into source control but rather have the CI server ( you DO use continuous integration, right?) generate the files when it performs the build

July 13, 2010

Using T4 to generate strongly typed translations in episerver (+ how to fire T4 on builds)

Filed under: Uncategorized — andreakn @ 12:49
Tags: , ,

Problem
Typically when developing with EPiServer in the past I have created a helper method to provide a shorthand for accessing language strings:

public static class Lang
{
  public static string Translate(string key)
  {
    //call standard EPiServer API
  }
  public static string Translate(string key, string defaultValue)
  {
    //call standard EPiServer API, return defaultValue if no match found
  }
}

the trouble is the key parameter, it is on the form “/Forms/RegisterForm/Firstname” for instance, which means that every developer needs to know which language strings have been defined and get their full xpath from the language strings (no spelling errors, please), this makes discovering common strings a bit painful as Visual studio does not provide the handy navigator in the bottom for xml files as it does for .aspx files.

I wanted to point T4 at one (or more) language file(s) and get generated accessors so I could get intellisense when accessing language strings.

Solution
what I ended up with was this T4 code (which I put into a file called EPiLanguageAccessors.tt:

<#@ template debug="true" hostSpecific="true" #>
<#@ assembly name="System.Xml"#>
<#@ assembly name="System.Xml.Linq" #>
<#@ assembly name="System.Core" #> 
<#@ import namespace="System.Xml.Linq"#>
<#@ import namespace="System.Linq"#>
<#@ import namespace="System.Collections.Generic"#>


<#
int skipLevels = 2; //depends on xml file, for standard episerver use 2
string xmlFilePath = @"\lang\lang.xml"; 
string baseCodeGenNamespace = "Lang";
XDocument xdoc = XDocument.Load(System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+xmlFilePath);

GenerateLanguageAccessors(xdoc.Root, new List<string>(),baseCodeGenNamespace,skipLevels, new List<string>());
        
#>

<#+

 void GenerateLanguageAccessors(XNode xNode, List<string> ns, string baseNS, int skipLevels, List<string> cache)
        {
            if (xNode is XText)
            {
                if (ns.Count < skipLevels) return;
                string theNameSpace = string.Join(".", ns.Skip(skipLevels).Take(ns.Count - (skipLevels)).ToArray());
                if (string.IsNullOrEmpty(theNameSpace))
                {
                    theNameSpace = baseNS;
                }
                else
                {
                    theNameSpace = baseNS + "." + theNameSpace;
                }
                string theKey = "/" + string.Join("/", ns.Skip(skipLevels).ToArray());
                string s = "namespace " + theNameSpace + "{ public static partial class Get{ public static string Text(){ return Utils.LanguageUtil.Translate(\"" + theKey + "\");} public static string TextOrDefault(string defaultString){ return Utils.LanguageUtil.Translate(\"" + theKey + "\",defaultString);} } }";
               	if(!cache.Contains(theNameSpace))
                {
                    this.WriteLine(s);
               	 	cache.Add(theNameSpace);
                }
				 return;
            }
            if (xNode is XElement)
            {
                var xElement = xNode as XElement;
                var newNameSpace = ns.ToArray().ToList();
                newNameSpace.Add(xElement.Name.ToString());
                foreach (var subNode in xElement.Nodes())
                {
                    GenerateLanguageAccessors(subNode, newNameSpace, baseNS, skipLevels, cache);
                }
            }
        }
		#>

The top part of the .tt file is needed to reference the right stuff for the text transformation,
the middle part is where you specify the specifics of what you want, for instance skiplevel is needed to skip “/language/language” which is a part of the xml files, but not used for accessing strings, also here is where I define the relative path to the language file which is the “master” which will be used for code generation.
the bottom part is the algorithm which creates namespaces for each string with translation logic.

so where previously I would have to use a magic string in the call to LanguageUtil.Translate("/App/Registration/Step1/EulaText"); I can now call Lang.App.Registration.Step1.EulaText.Get.Text();

Getting it to run on each build

T4 out of the box runs the transformation each time the template file is changed. In our case that is not too useful, as we would rather have the transformation run each time the “master” language file is changed. AFAIK it isn’t possible to instruct T4 to fire when some custom file is changed, but you can get it to fire on each build.

There are a few options, for a run down you might want to head over to http://www.olegsych.com which is a goldmine of T4 information.
I found the simplest option to spread across a team (no need to install anything extra to the dev machines) is to create a file called FireT4OnBuild.targets, put it into the root of the project and populate it like this:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="ExecuteT4Templates">
        <ItemGroup>
            <T4Templates Include=".\**\*.tt" />
        </ItemGroup>
         <Exec
            WorkingDirectory="C:\Program Files (x86)\Common Files\microsoft shared\TextTemplating\10.0\"
            Command="TextTransform &quot;%(T4Templates.FullPath)&quot; -out &quot;%(T4Templates.RootDir)%(T4Templates.Directory)%(T4Templates.Filename).cs&quot; " /> 
    </Target>
</Project>

You might want to change the path for the working directory (my setup is for .net 4.0 running on windows 7 (64bit)
if the devs use different setups you might want to copy the texttemplating files to a common path (SolutionDir/Lib/T4 for instance) and reference that path

Then you need to unload the project containing the transformation (I’m guessing your xxxxx.Web.csproj ) and edit the .csproj file (right click => unload, then right click => edit )
and add the line

 <Import Project="$(MSBuildProjectDirectory)\FireT4OnBuild.targets" />

in the bottom of the file (put it after the import of Microsoft.CSharp.targets) and changing the defaulttargets to “ExecuteT4Templates;Build”

Now T4 should be firing every time you touch the .tt file(s) and also every time you perform a build. Since ExecuteT4Templates is put before Build the generated code will be in place in time for the build

« Previous PageNext Page »

Theme: Rubric. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.