ASP.NET MVC with Windsor – programmatic controller registration

March 28, 2009 at 11:29 AMAndre Loker

Although I’m a loyal MonoRail user, I’m playing with ASP.NET MVC a bit sometimes if time permits. One of the first things I wanted to do is using an IoC container such as Windsor to resolve controllers. ASP.NET MVC was built with extensibility in mind, so that’s not much of a problem, and it has been written about, for example by Matt Hall.

The problem is that the articles I read use the Windsor XML configuration for this. Frankly, I don’t like to configure my IoC container using XML if I don’t have to. Its syntax is pretty verbose and honestly I don’t need the controllers to be configured externally. Therefore, I prefer to use programmatic registration. Windsor has such a sweet registration API, you got to love it.

Alright, so what do you have to do to make programmatically registered controllers available to ASP.NET MVC?

Step 1 – create the container instance

This is easy: in the Application_Start event create the WindsorContainer and store it in a static(!) field. It has to be static, because more than one instance of the HttpApplication can potentially be created.

 

   1: public class MvcApplication : System.Web.HttpApplication {
   2:   static IWindsorContainer container;
   3:  
   4:   protected void Application_Start() {
   5:     CreateWindsorContainer();
   6:     RegisterRoutes(RouteTable.Routes); // has been there before!
   7:   }
   8:  
   9:   static void CreateWindsorContainer() {
  10:     container = new WindsorContainer();
  11:   }
  12: }

Did I mention that the container field needs to be static?

Step 2 – register controllers

This is where the nice API of Windsor comes into play. Extend the application as follows:

   1: public class MvcApplication : System.Web.HttpApplication{
   2:   ...
   3:  
   4:   protected void Application_Start() {
   5:     CreateWindsorContainer();
   6:     RegisterRoutes(RouteTable.Routes);
   7:     RegisterControllers(); // added
   8:   }
   9:  
  10:   ...
  11:  
  12:   static void RegisterControllers() {
  13:     container.Register(
  14:       AllTypes
  15:         .FromAssembly(Assembly.GetExecutingAssembly())
  16:         .BasedOn<IController>()        
  17:     );
  18:   }
  19: }

This is simple, isn’t it? No per-controller-entry in some XML file, just a simple piece of code.

Step 3 – create your own controller factory

To create the controller instances, ASP.NET MVC uses an object that implement IControllerFactory. So all we need to do is roll our own controller factory that uses Windsor and tell ASP.NET MVC to use that.

OK, so here’s our controller factory:

   1: public class WindsorControllerFactory : IControllerFactory {
   2:  
   3:   readonly IWindsorContainer container;
   4:  
   5:   public WindsorControllerFactory(IWindsorContainer container) {
   6:     this.container = container;
   7:   }
   8:  
   9:   public IController CreateController(RequestContext requestContext, string controllerName) {
  10:     var componentName = GetComponentNameFromControllerName(controllerName);
  11:     return container.Resolve<IController>(componentName);
  12:   }
  13:  
  14:   public void ReleaseController(IController controller) {
  15:     container.Release(controller);
  16:   }
  17:  
  18:   /// <summary>
  19:   /// Maps from a simple controller name to the name of the component
  20:   /// that implements the controller.
  21:   /// </summary>
  22:   /// <param name="controllerName">Name of the controller.</param>
  23:   /// <returns>Name of the controller component.</returns>
  24:   static string GetComponentNameFromControllerName(string controllerName) {
  25:     var controllerNamespace = typeof(HomeController).Namespace;
  26:     return string.Format("{0}.{1}Controller", controllerNamespace, controllerName);
  27:   }
  28: }

Nothing fancy here: first, we’ll need the windsor container instance, so we pass it as a constructor dependency. CreateController and ReleaseController are the two methods of IControllerFactory – their purpose should be self-explanatory. GetComponentNameFromControllerName maybe needs some further explanation. ASP.NET MVC will ask for controllers by their simple name, that is “Home” or “About” etc. However, by the way we registered the controller components Windsor knows them by their full type name, e.g. “MyApplication.Controllers.HomeController” and “MyApplication.Controllers.AboutController”. GetComponentNameFromControllerName simply converts from the simple controller name to the full component name.

Step 4 – tell ASP.NET MVC to use our controller factory

The last step is to tell ASP.NET MVC to actually use our IControllerFactory implementation instead of the default one. Conceptually this is easy, we only need to call ControllerBuilder.Current.SetControllerFactory and pass it either the type of our IControllerFactory implementation or an instance. But let’s not be too quick here and try to keep our application as DI-ish as possible. Here’s how I’d set the controller factory:

   1: public class MvcApplication : System.Web.HttpApplication{
   2:   ...
   3:  
   4:   protected void Application_Start() {
   5:     CreateWindsorContainer();
   6:     RegisterRoutes(RouteTable.Routes);
   7:     RegisterControllers();
   8:     RegisterControllerFactory(); // new
   9:   }
  10:   
  11:   void RegisterControllerFactory() {
  12:     container.Register(
  13:       Component
  14:         .For<IControllerFactory>()
  15:         .ImplementedBy<WindsorControllerFactory>()
  16:         .LifeStyle.Singleton
  17:       );
  18:     var controllerFactory = container.Resolve<IControllerFactory>();
  19:     ControllerBuilder.Current.SetControllerFactory(controllerFactory);
  20:   }
  21:  
  22:   static void CreateWindsorContainer() {
  23:     container = new WindsorContainer();
  24:     // new: register the container with itself 
  25:     //      to be able to resolve the dependency in the ctor
  26:     //      of WindsorControllerFactory
  27:     container.Register(
  28:       Component
  29:         .For<IWindsorContainer>()
  30:         .Instance(container)
  31:       );
  32:   }  
  33:  
  34:   ...
  35: }

You see, instead of new-ing an instance of WIndsorControllerFactory we register it as a singleton component and resolve it. Also note that we needed to register the Windsor container with itself to have the constructor dependency resolved.

Finally, here’s the complete source code of the global application class so far:

   1: public class MvcApplication : System.Web.HttpApplication {
   2:   static IWindsorContainer container;
   3:  
   4:   public static void RegisterRoutes(RouteCollection routes) {
   5:     routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
   6:  
   7:     routes.MapRoute(
   8:       "Default", // Route name
   9:       "{controller}/{action}/{id}", // URL with parameters
  10:       new { controller = "Home", action = "Index", id = "" } // Parameter defaults
  11:       );
  12:   }
  13:  
  14:   protected void Application_Start() {
  15:     CreateWindsorContainer();
  16:     RegisterRoutes(RouteTable.Routes);
  17:     RegisterControllers();
  18:     RegisterControllerFactory();
  19:   }
  20:  
  21:   static void CreateWindsorContainer() {
  22:     container = new WindsorContainer();
  23:     container.Register(
  24:       Component
  25:         .For<IWindsorContainer>()
  26:         .Instance(container)
  27:       );
  28:   }
  29:  
  30:   static void RegisterControllers() {
  31:     container.Register(
  32:       AllTypes
  33:         .FromAssembly(Assembly.GetExecutingAssembly())
  34:         .BasedOn<IController>()
  35:       );
  36:   }
  37:  
  38:   static void RegisterControllerFactory() {
  39:     container.Register(
  40:       Component
  41:         .For<IControllerFactory>()
  42:         .ImplementedBy<WindsorControllerFactory>()
  43:         .LifeStyle.Singleton
  44:       );
  45:     var controllerFactory = container.Resolve<IControllerFactory>();
  46:     ControllerBuilder.Current.SetControllerFactory(controllerFactory);
  47:   }
  48: }

Controller names and component names revisited

As you saw in step 3, we need to map the controller names to component names. This doesn’t take much effort and is my preferred way of handling this. But of course, you can already register the controller components with the respective controller names in the first place if you like. Just change the controller registration to something like this:

   1: static void RegisterControllers() {
   2:   container.Register(
   3:     AllTypes
   4:       .FromAssembly(Assembly.GetExecutingAssembly())
   5:       .BasedOn<IController>()
   6:       // modify the name with which the component is registered:
   7:       .Configure(component => component.Named(ControllerNameFromType(component.Implementation)))
   8:     );
   9: }
  10:  
  11: static string ControllerNameFromType(Type implementation) {
  12:   const string ControllerSuffix = "Controller";
  13:   var name = implementation.Name;
  14:   Debug.Assert(name.EndsWith(ControllerSuffix));
  15:   return name.Substring(0, name.Length - ControllerSuffix.Length);
  16: }

If you do this, you can remove the GetComponentNameFromControllerName method and simplify the implementation of IControllerFactory.CreateController to this:

   1: public IController CreateController(RequestContext requestContext, 
   2:                                     string controllerName) {
   3:   return container.Resolve<IController>(controllerName);
   4: }

Which version you choose is more or less a matter of taste. If you keep the full type name as the component’s name you reduce the chance for name clashes. However, if you register the components using the controller name creating the controller is slightly simpler.

More simplifications

You can even simplify the controller factory a bit if you inherit from DefaultControllerFactory instead of implementing IControllerFactory. Because DefaultControllerFactory will resolve the correct component type for you, you can simply override GetControllerInstance instead of implementing CreateController:

   1: public class WindsorControllerFactory : DefaultControllerFactory {
   2:   readonly IWindsorContainer container;
   3:  
   4:   public WindsorControllerFactory(IWindsorContainer container) {
   5:     this.container = container;
   6:   }
   7:  
   8:   protected override IController GetControllerInstance(System.Type controllerType) {
   9:     return container.Resolve(controllerType) as IController;
  10:   }
  11:  
  12:   public override void ReleaseController(IController controller) {
  13:     container.Release(controller);
  14:   }
  15: }

If you need to change the way that the controller name is mapped to a component type, you can also override GetControllerType.

MVC Contrib

As far as I know Windsor integration is also part of the MVC Contrib project. I haven’t looked into it, so bear with me if this article doubles existing code!

Posted in: ASP.NET | Castle

Tags: , , ,

Post-Redirect-Get

June 27, 2008 at 5:01 PMAndre Loker

HTTP methods

As you probably know, HTTP supports several methods that define the nature of the current request. The two most important ones are GET and POST. GET is the primary method to get content (so called entities) from the server such as  HTML pages, images, CSS style sheets etc. The POST method on the other hand is meant to transport entities to the server, for example login credentials or a blog comment. On the server side a POST request often results in an update of certain data (databases, session state).

Both GET and POST can return an entity as a response. For GET this is obvious - it's what the method exists for in the first place. For POST it might sound reasonable in the first place as well, but it brings a pile of problems.

A simple scenario

Imagine you fill in a sign-up form of some web based e-mail service and POST it to the server using a submit button. The server processes the new account and updates its database. Maybe it even logs you in directly. In response of the POST request the server directly shows you a view of your inbox. Here's a diagram of what happens between browser and server:

 image

  1. The browser POSTs form data to an URL called signup.aspx
  2. The server processes the request
  3. The server responds with a status code of 200 (OK) and sends back a view of the new users inbox rendered as HTML

You leave the computer to have a coffee and when you come back 5 minutes later you refresh the page (using CTRL+R or F5 or whatever shortcut your browser uses) to see whether you already have new messages. You are a bit puzzled why this (or a similar) message box appears:

image

You click on OK and are even more confused as the page that appears says "This user name is already taken" instead of showing your inbox .

What has happened? Remember that the page you saw was the response of a POST request (submitting the sign up form). When you refreshed the page and confirmed to "resend the data" you actually repeated the POST request with the same form data. The server processed the "new" account and found that the user name is already in use (by yourself!), therefore it showed an error. "But wait", you say, "I just wanted the server to refresh the view of my inbox, what have I done wrong? " The answer is: nothing! The problem is that the application abused the POST response to transport an entity back to the client that should have been accessed with a GET request in the first place.

POST related issues

Here are some of the problems that occur if you abuse POST requests to return entities:

1. Refreshing the page results in a re-transmission of the POST data

This is what I described above. Hitting "refresh page" for a reponse based on a POST request will re-issue the POST request. Instead of refreshing what you see this will repeat what you did to reach the current page. This is not "refresh page" anymore, it becomes "repeat last action" - which is most likely not what the user wants. If you see a summary page after you have submitted an order in an online store, you don't want F5 to drop another order, do you?

2. POST responses are hard to bookmark

Bookmarks (or favourites etc.) normally only remember the URL of the bookmarked page (along with some user supplied meta data). Because a POST request transports data in the request body instead as query parameters in the URL like GET does, bookmarking the result of a POST will not work in most cases.

3. POST responses pollute the browser history

If the browser keeps the result of a POST request in it's history, going back to that history entry will normally result in POST data to be retransmitted. This again causes the same issues as mentioned in point 1.

POST-Redirect-GET

"But I need POSTs to send forms to the server - how can I avoid the problems mentioned above?" you might say. Here's where the POST-Redirect-GET (PRG hereafter) pattern enters the stage.

Instead of sending entity content with the POST response after we processed the request, we return the URL of a new location which the browser should visit afterwards. Normally this new location shows the result of the POST or an updated view of some domain model.

This can be achieved by not returning a result code of 200 (success) but instead returning a status code that indicates a new location for the result, for example 303 ("See other") or 302("Found"/"Moved temporarily"), the latter of which is used most often nowadays. Together with the 30x result code a Location header is sent which contains the URL of the page to which the request is redirected. Only the headers are sent, no body is included.

If the browser sees the 30x status code, it will look for the Location header and issue a GET request to the URL mentioned there. Finally the user will see the body of that GET request in the browser.

The browser-server communication would look like this:

 image

  1. The browser POSTs to signup.aspx
  2. The server updates some state etc.
  3. The response is 302 redirect with a Location header value of inbox.aspx
  4. The browser realizes that the response is redirected and issues a GET to inbox.aspx
  5. The server returns 200 together with the content of the resource.

What do we gain?

  • The page can be safely refreshed. Refreshing will cause another GET to inbox.aspx which won't cause any updates on the server
  • The result page can be easily bookmarked. Because the current resource is defined by the URL a bookmark to this URL is likely to be valid.
  • The browser history stays clean. Responses that have a redirect status code (such as 302) will not be put into the browser cache by most browsers. Only the location to which the response is redirecting is. Therefore signup.aspx won't be added to the history and we can safely go back and forth through the history without having to resubmit any POST data

The drawbacks of POST-Redirect-GET

While it should be clear by now that the POST-Redirect-GET pattern is the way to go in most situations, I'd like to point at the few drawbacks that come along with this pattern.

First of all, redirection from one request to another causes an extra roundtrip to the server (one for the POST request, one for the GET request it redirects to). In this context the roundtrip should be understood as all processing and transmission time that is required and fixed per request, ie. transmission delay, creation and invation of the HTTP handler, opening and closing database connections/transactions, filling ORM caches etc.

If both requests can be handled very quickly by the server this will essentially double the response time. If your roundtrip time is 200ms, using PRG will cause a minimum delay of 400ms between submitting the form and the result page being visible. This issues has to be put in perspective with reality, however. The server will need some time processing both requests, so the percentage of time needed for the roundtrips decreases with the amount of time server processing time takes. The response from the POST itself can be extremely small (few hundred bytes), because only the headers need to be transmitted.

In practice I haven't noticed a real performance problem with PRG. A slow app will stay slow, a fast one won't truly suffer from the extra roundtrip. And besides, if you replace POSTs by GETs where appropriate the effect of PRG will be even less noticeable.

The problem with ASP.NET WebForms

Now that you know about POST-Redirect-GET you are of course eager to use it (at least I hope I could convince you). But as an ASP.NET WebForms developer you will soon run into problems: ASP.NET WebForms is fundamentally based on POSTs to the server. In essence, all ASP.NET web pages are wrapped in one huge <form> element with "method" set to "POST". Whenever you click a button, you essentially POST all form fields to the server. Of course you can redirect from a Button.Click handler. If you do so, you're applying PRG. At the same time you're working quite against the WebForms philosophy, especially the ViewState (which will get lost as soon as you redirect), which will force you to rethink a lot of your application logic. And if you don't rely on all this postback behaviour inherent to ASP.NET WebForms you might as well ask why you're using WebForms in the first place.

This makes clear why a lot of developers (including me) think that WebForms are inherently "broken" (viewstate, ubiquitous postbacks and the hard-to-mock HttpContext are just a few reasons). If you share these concerns but like .NET just as I do, you might want to look at alternate .NET based web frameworks such as Castle MonoRail or ASP.NET MVC.

PRG and AJAX

In situations where you use AJAX the whole PRG issue becomes a new story. AJAX responses don't appear in the history, you wouldn't want to bookmark them and refreshing a web page does not re-issue any AJAX requests (except those fired on page load). Therefore I have no problem with returning entitiest (HTML fragments, JSON, XML) from AJAX POSTs - PRG is not of much use here.

Conclusion

To conclude this article here's a list of some basic rules that have been useful to me:

  1. Use POST-Redirect-GET whenever you can, that is: whenever you process a POST request on the server, send a redirect to a GETtable resource as response. It's applicable in almost all cases and will make your site much more usable for the visitor
  2. Don't POST what you can GET. If you only want to retrieve a parameterised resource it might be completely suitable to use a GET request with query string parameters. Google is a good example. The start page contains a simple form with a single text field to enter the search terms. Submitting the form causes a GET to /search with the search terms passed as the query string parameter q. This can be easily done by providing method="GET" on the <form> element (or just leave out the method attribute, as GET is the default).
  3. POST requests from AJAX are allowed to return entities directly as they don't suffer from the problems like "full" POSTs.

Posted in: ASP.NET | Patterns

Tags: , , ,

Keep your .config clean with external config files

June 16, 2008 at 1:25 PMAndre 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

Posted in: ASP.NET

Tags: , ,

HttpApplication instances

May 18, 2008 at 8:42 PMAndre 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

Posted in: ASP.NET

Tags: , ,

Effizienz primitiver Datentypen im ControlState

February 25, 2008 at 10:10 PMAndre 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.

Posted in: ASP.NET

Tags: , ,