Keep your .config clean with external config files

June 16, 2008 13:25 by Andre Loker

The web.config (or app.config for non-web applications) file is the central place to configure your web application, starting from connection strings, over application settings to ASP.NET specific topics like caching, authentication & authorization, sessions as well as HTTP handlers and modules. Normally the web.config starts as a neat little pet you can easily manage. But as soon as your project grows mature, web.config turns out to be a huge beast. The size itself is often not that much of a problem. An issue I personally find more important is the fact that I need my web.config files to be different for my developer's machine and the deployment scenarios: on my dev machine I certainly have different connection strings than on the production server, the same counts for app settings, logging, compilation settings etc. pp.

Maintaining multiple configuration files is a PITA, especially when it comes to the configuration parts that have to be similar on all config files (e.g. http handlers and modules will most likely be the same).

Luckily the .NET configuration system has a feature that drastically lightens the burden for us. Each configuration section may define an attribute named "configSource" to define an alternate location from where the configuration has to be loaded. Let's have a look at an example.

In web.config locate or create the connectionStrings section. Remove all child elements (<add>, <remove> or <clear>) and add a new attribute named configSource. Give it the name of a file called "ConnectionStrings.config" as its value:

   1: <connectionStrings configSource="ConnectionStrings.config"/>

Add a new file called "ConnectionStrings.config" to the same directory where web.config lives and define its content as follows:

   1: <?xml version="1.0"?>
   2: <connectionStrings>
   3:   <add name="MyDB" connectionString="Data Source=myServerAddress;Initial Catalog=MyDataBase;Integrated Security=SSPI;"/>
   4: </connectionStrings>

That's it. Now the content of ConnectionStrings.config will be used for the connectionStrings section.

Some things to note:

  • The external configuration file must contain the section's name as the root element, i.e. an external config file for the <smtp> element must have the <smtp> node as it's root even though the element is nested within system.net/mailSettings in the original web.config.
  • If you use configSource you must not define any other attribute or child element for the respective section in web.config. This means that configSource can never be used to add additional content to a section. It will always replace the section.
  • It might be useful to move configuration files to a subfolder to keep the root directory clean. I personally use a subfolder named Config. The configSource attributes have to include the folder in that case, like configSource="Config\connectionStrings.config"
  • The actual file extension of the external configuration files does not matter (as long as you point to the correct file in the configSource attribute). Keep in mind, though, that depending on the extension the web server might or might not serve the literal content of the config files when requested. If the config files contain sensitive information (DB passwords etc.) this can be a massive security risk. .config is one of the extensions IIS/ASP.NET will refuse serve to the client. Therefore I recommend to always give configuration files the extension ".config".

Multiple deployment scenarios

Now that we are able to move configuration settings to an external source file the web.config becomes easier to manage: if for example we need to have two different sets of connection strings (or app settings etc.) for different deployment scenarios we can define multiple external configuration files, each one for a different scenario, like: connectionStrings-dev.config for my dev machine and connectionStrings-deploy.config for the production server. All I have to do to switch between these two is to modify the configSource in the web.config. (BTW: the web deployment projects can do this automatically for you, just check the "Enable Web.config file section replacement" checkbox under the "Deployment" tab and define the new file names; see Extreme ASP.NET: Web Deployment Projects under "Pluggable Configuration Files").

Hiding sensitive information

If you're working on an open source project that uses a shared code repository you may not want specific information to appear in the repository, like database connection strings, smtp credentials. If you move those settings to external configuration files it is easy to exclude them from your repository.

Non-standard config sections

ConfigSource is a feature of the .NET configuration system and not specific to the ASP.NET configuration sections. Therefore it should work with other config sections as well.

Recommendations

Personally I recommend to move the following sections to external configuration files:

  • appSettings - likely to change for different deployment scenarios
  • connectionStrings - likely to change for different deployment scenarios
  • smtp in system.net/mailSettings - for security reasons and because it's likely to change for different deployment scenarios
  • machineKey in system.web - primarily for security reasons
  • External libraries like log4net, Castle Windsor, NHibernate etc

HttpApplication instances

May 18, 2008 20:42 by Andre Loker

I always thought the HttpApplication class defined in global.asax would only be instantiated once for the web application. This is not the case. ASP.NET may instantiate many instances of HttpApplication as required. In fact, it will create an instance of the class for each request that is handled in parallel on the server. That is, seven requests being executed at the same time will result in at least seven instances of the application class.

Important facts:

  • Multiple HttpApplication instances can be created by ASP.NET during runtime
    • At least one for each parallel request
  • Each HttpApplication will handle a single request
    • No two parallel requests will have access to the same HttpApplication instance
    • This means that instance fields on the HttpApplication are thread safe and can be used exclusively for the current HttpRequest
  • HttpApplication instances are reused
    • The same HttpApplication instance is allowed to be used for multiple (consecutive, not concurrent) requests
    • This means that the application instance might be around for quite some time without being garbage collected. If you use instance fields that reference expensive resources you should clean up those instance fields in Application_EndRequest
  • Application_Start and Application_End are called only once per running web application. Of course, each time the application pool is recycled or the webapplication is restarted the two events are called again.
    • The default template for global.asax.cs implements Application_Start and Application_End as instance methods of Global (the class derived from HttpApplication). This is misleading as the programmer might be tempted to initialize instance fields in those event handlers. As the handlers are called only once on the first instance of the HttpApplication class all subsequently created instances of the class won't have their instance fields initialized correctly (i.e. with the semantic as implied by the Application_Start implementation)
    • Conclusion: do not set any instance fields in Application_Start or Application_End unless you don't need them for more then one (i.e. the first) request

Effizienz primitiver Datentypen im ControlState

February 25, 2008 22:10 by Andre Loker

ASP.NET 2 bietet neben dem ViewState auch den so genannten ControlState an, um Control-Zustände über Postbacks hinweg zu speichern. In der Standardeinstellung wird der ControlState wie der ViewState in das versteckte Feld "__VIEWSTATE" der Seite serialisiert. Informationen dazu, wie man ControlStates verwendet, gibt es genügend im Internet.

Durch Zufall bin ich heute aber auf eine interessante Tatsache gestoßen. Natürlich sollten die Daten des ControlStates so klein wie möglich gehalten werden. In meinem konkreten Fall musste das Control lediglich die ID aus einer Datenbank speichern. Der Wert war vom Typ Int64, was 8 bytes zur Laufzeit entspricht. Ich erwartete also etwas mehr als diese 8 bytes im ControlState. Zur Kontrolle habe ich mir trace.axd zur Hilfe genommen, um die Größe des ControlStates zu überprüfen - und war doch ziemlich überrascht, dass dieser eine Int64 sage und schreibe 132 bytes veranschlagte. Das schien mir doch reichlich viel und veranlasste mich dazu, genauer zu hinterfragen, warum so der Wert so groß ist.

Versuchsweise verwendete ich statt des Int64 einen "gewöhnlichen" Int32 und erhielt als ControlState Größe nunmehr lediglich 8 bytes. Mir wurde dann relativ schnell klar, dass die Wahl des Datentyps von entscheidender Bedeutung ist. Ich habe also eine Testreihe gestartet, in der ich verschiedenste primitive Datentypen als ControlState verwendete. Dabei wurde auch deutlich, dass nicht nur der Datentyp eine Rolle spielte, sondern auch der Wert(!). Kleine Integer wurden offenbar kompakter serialisiert als große.

Ergebnisse

Ganzzahlige und Fließkommadatentypen

Die erste "Disziplin" ist das Speichern einfacher numerischer Datentypen, sowohl ganzzahlige Typen als auch Fließkommatypen. Getestet habe ich einmal den Wert "0" für jeden Datentyp und den höchsten Wert, welcher durch das konstante Feld MaxValue bei allen Typen angegeben ist.

Datentyp ControlState:
Wert 0
ControlState:
MaxValue
byte 8 8
sbyte 132 136
short 12 12
ushort 132 140
int 8 16
uint 132 144
long 132 156
ulong 132 160
float 12 12
double 20 20
decimal 136 172

 

Ergebnis: Byte, short, int float und double werden gesondert behandelt und sehr effizient gespeichert. Alle vorzeichenlosen Ganzzahlen (Ausnahme: bytes - dort sind es die vorzeichenbehafteten) sowie long, ulong und decimal sind "teuer". Für sie besteht offensichtlich keine effiziente Serialisierung im Framework.

 

Strings und Booleans

Strings und Boolsche Werte.

Wert ControlState
true 8
false 8
"test" 16
string.Empty 8

 

Ergebnis: sowohl Boolsche Werte als auch Strings werden sehr effizient serialisiert.

 

arrays

Falls man mehrere Werte als ControlState speichern möchte, wäre es ggf. interessant, diese als Array zu serialisieren. Ich habe einfache object-Arrays getestet als auch einige spezialisierte arrays (byte und int).

Array inhalt ControlState:
Wert 0 bzw String.Empty
ControlState:
MaxValue bzw. "test
"
object[] { int } 12 20
object[] { int, int } 16 28
object[] { int, int, int } 16 36
object[] { int, int, int, int } 16 44
object[] { byte } 16 16
byte[] { byte } 132 132
int[] { int } 12 20
string[] {string } 12 16

 

Ergebnis: object-Arrays scheinen sehr kompakt gespeichert zu werden. Ebenso sind int und string Arrays sehr effizient. Andere Arraytypen scheinen nicht gesondert behandelt zu werden.

 

8 bytes als speichern

In meinem konkreten Beispiel möchte ich einen Int64 als ControlState speichern. Die folgende Tabelle zeigt, wie ich dieses 8 bytes ggf. auch als ein Array von kleineren Zahlenwerten speichern könnte, indem ich die 8 bytes aufteile.

Array ControlState:
Wert 0
ControlState:
MaxValue
object[] { 8x byte } 32 32
object[] { 4x short } 28 28
object[] { 2x int } 16 28

 

Ergebnis: wie schon zuvor siegt der Int32-Typ. Doch selbst, wenn der Int64 Wert als ein Array von 8 bytes gespeichert wird, beträgt die Größe höchstens ein Viertel im Vergleich zum serialisierten Int64

 

Pair und Triplet

Als Alternative zum Array bieten sich für ControlStates, die aus zwei oder drei Werten bestehen, die Typen Pair und Triplet im System.Web.UI Namespace an. Die folgende Tabelle zeigt die ControlState Größe bei Verwendung des Pairs oder Triplets und zum Vergleich dazu noch einmal gleichwertige Arrays mit zwei bzw. drei Werten.

Inhalt ControlState:
Wert 0
ControlState:
MaxValue
Pair(int, int) 12 24
object[] { int, int } 16 28
Triplet(int, int, int) 12 32
object[] { int, int, int } 16 36

 

Ergebnis: offensichtlich sind Pair und Triplet nochmal jeweils 4 bytes kleiner als ihr Array-Pendant.

Fazit

Es ist offensichtlich, dass einige Datentypen gesondert serilisiert werden. Mit einer klugen Auswahl des Datentypes, kann die Größe des ControlStates dramatisch verkleinert werde, je nach Fall zwischen 75% und 90%.

Einige Richtlinien, die man aus den Tabellen entnehmen kann:

  • int (Int32) ist generell der bevorzugte Datentyp für Ganzzahlen.
  • Im ControlState zu empfehlen sind nur folgende primitive Typen: byte, short, int, float, double, bool und string.
  • Um zwei oder drei Werte zu speichern, sollten Pair bzw. Triplet verwendet werden
  • Bei mehr als drei Werten können Arrays verwendet werden. In der Regel sollte das ein object[] Array sein. Bei int und string sind int[] respektive string[] gleich effizient gegenüber object[].
  • 64 bit Integer sollten zum Speichern mittels Bitmaske in 2 32 bit Integer geteilt und als Pair gespeichert werden
  • sbyte, ushort und ulong sollten zunächst auf Bitniveau in die effizienten Pendants byte, short und long gebracht werden.

Definition von __doPostBack erzwingen

February 15, 2008 14:13 by Andre Loker

Das Problem

Zwei aspx-Seiten, beide rufen den identischen JavaScript Code auf:

   1: function postBackHiddenField(hiddenFieldID) {
   2:     var hiddenField = $get(hiddenFieldID);
   3:     if (hiddenField) {
   4:         hiddenField.value = (new Date()).getTime();
   5:         __doPostBack(hiddenFieldID,'');
   6:     }
   7: }

Auf Seite A funktioniert der JS Code, auf Seite B nicht. Bei letzterem sagt der JavaScript-Debugger:

__doPostBack not defined

(Hoffentlich) verständlicherweise verursachte das bei mir ziemliches Kopfkratzen. __doPostBack sollte ja eigentlich auf einer ASPX Seite nicht fremd sein. Bis mir auf einmal siedendheiß einfiel, dass ASP.NET vielleicht so schlau ist, und nur dann die enstprechenden Script-Dateien inkludiert, wenn tatsächlich ein Control auch ein Postback via JavaScript ausführen kann. Auf Seite A war so ein Control vorhanden, auf Seite B jedoch nicht. Demzufolge wurde für Seite B der Code für __doPostBack nicht in die Seite eingepflegt.

Der Quick-Fix

Um das Problem zu lösen, reicht es schon aus, einfach ein Control einzufügen, dass einen Postback mittels JavaScript auslöst, wie etwa ein LinkButton. Dieser muss auf der Client Seite nicht zwingend sichtbar sein, d.h. man kann ihn clientseitig ausblenden:

   1: <asp:LinkButton runat="server" Style='display: none;' ID="enablesJSPostBack" />

Achtung: es funktioniert nicht, wenn man den LinkButton schon serverseitig ausblendet mit Visible="false"! In dem Fall erzeugt ASP.NET ebenfalls den nötigen Code für __doPostBack nicht.

Technische Details und eine elegantere Lösung

Ich habe ein wenig mit Hilfe von Reflector im Gedärm von ASP.NET gewühlt um nachzusehen, unter welchen Umständen der __doPostBack-Code gerendert wird. Womöglich gibt es eine Methode, mit der man eben dieses erreichen kann, ohne ein Control in der Seite erstellen zu müssen. Entscheidend ist die Methode RegisterPostBackScript der Page-Klasse. Wird sie angerufen erzeugt die Seite den __doPostBack Code. Leider ist die Methode internal. Allerdings wird die Methode unter anderem aufgerufen, wenn man ClientScript.GetPostBackEventReference anruft. Mit dieser Methode kann man den __doPostBack Script-Code erzeugen, der einen Script-seitiges Postback ausführt. Ich habe meiner Page-Klasse folgende Methode hinzugefügt, um (ohne zusätzliche Controls) sicher zu stellen, dass das __doPostBack-Script erzeugt wird:

   1: protected void EnsureDoPostBackDefined(){
   2:     ClientScript.GetPostBackEventReference(this, string.Empty);
   3: }

Die Methode EnsureDoPostBackDefined wird einfach z.B. im Load Ereignis aufgerufen und schon klappt es mit __doPostBack.