Reflection als WCF inpakpapier

Kijk hier voor het originele SDN artikel.

Sander van de Velde heeft een oplossing bedacht voor het runtime inpakken van
een WCF interface waarbij de ingepakte server een andere interface definitie dan
de WCF interface ondersteunt. De twee interfaces moeten alleen dezelfde methodes bezitten. Sander maakt hierbij gebruik van het wrapper design pattern en Microsoft Intermediate Language generatie. Dit biedt de mogelijkheid om zelfs een Service locator te bouwen waarbij verschillende services dezelfde interface ondersteunen.

Interface en methode contracten

Tijdens een recentelijk project is een service eerst in Windows Communication
Foundation (WCF) en later met Remoting geïmplementeerd. De webserver buiten de firewall (demilitarized zone, DMZ) moest hierbij als cliënt een applicatieserver binnen de firewall aanroepen. De communicatie was in eerste instantie in WCF ontwikkeld. Een WCF service is zo gebouwd, compleet met de WCF attributen ServiceContract en OperationContract op de bijbehorende methodes van de interface beschrijving. Helaas bleek in een later stadium dat op de webserver hoogstens het .Net 2.0 framework beschikbaar was dus moesten we voor de communicatie uitwijken naar .Net Remoting.
Met .Net Remoting was dezelfde oplossing met dezelfde methodes eigenlijk net zo eenvoudig gebouwd maar er waren wel enkele duidelijke verschillen met WCF welke direct opvielen. Ten eerste werd een design time gegeneerde cliënt proxy
opgedrongen waar Remoting volstaat met enkele regels code om een cliënt proxy te genereren. Bij iedere wijziging van het servicecontract moest de WCF cliënt
proxy opnieuw bijgewerkt worden. Gelukkig bleek deze cliënt proxy toch ook te
genereren. Door het gebruik van een ChannelFactory kan dezelfde functionaliteit
geboden worden (zie listing 1).

[ServiceContract]
public interface IWcfService
{
  [OperationContract]
  string DoWork();
}
…
ChannelFactory<IWcfService> factory =
  new ChannelFactory<IWcfService>(new WSHttpBinding()))
IWcfService service =
  factory.CreateChannel(new EndpointAddress(
    "http://localhost/WcfHost/Service.svc"));
string antwoord = service.DoWork();
((ICommunicationObject)service).Close();
factory.Close();
Listing 1: Een echte WCF aanroep zonder vooraf gegenereerde proxy

Bij WCF maken de cliënt proxy en de server gebruik van dezelfde service
interface compleet met de WCF attributen. Hierdoor viel het op dat op de cliënt,
de aanroepende partij, het nodig is om een referentie naar System.ServiceModel
te leggen. Nu lijkt dit op zich triviaal; deze assembly is namelijk gewoon
geïnstalleerd met het .Net framework en dus altijd overal aanwezig. Maar het is
wel vreemd dat de gebruikte manier van communiceren zichtbaar wordt voor de
aanroepende partij en dat dus afgedwongen wordt dat bepaalde gerefereerde
assemblies geladen moeten worden. De WCF interface definitie is dus opdringerig, een cliënt moet niets merken over de manier van communiceren.

De WCF interface definitie is dus opdringerig, een cliënt moet
niets merken over de manier van communiceren.

Factory design pattern

Dit kan opgelost worden door gebruik te maken van een factory design pattern
voor de communicatie. Design patterns zijn standaard oplossingen voor standaard problemen (zoals hier de wens tot abstractie van de communicatie) en binnen Atos Origin proberen wij altijd eerst terug te grijpen op deze bewezen
oplossingen.

Figuur 1: Class diagram van factory pattern. De twee interface definities zijn helaas niet gelijk

Bij een factory design pattern wordt een instantie van de interface opgevraagd
om bepaalde werkzaamheden uit te voeren, zonder kennis te nemen van de
daadwerkelijke implementatie. De beslissing welke klasse wordt geïnstancieerd
zal door de factory gemaakt worden. De factory kan bijvoorbeeld afhankelijk van
het aanwezige .Net platform de keuze tussen .Net Remoting of WCF maken (zie
figuur 1). Maar omdat de twee genoemde technieken ieder een eigen interface
definitie vereisen, met of zonder WCF attributen, moet hier een tweede design
pattern toegepast worden.

Wrapper design pattern

Het is dus niet mogelijk om WCF zonder de benodigde WCF attributen te laten
communiceren. Een WCF cliënt proxy moet de benodigde kennis over de WCF
interface bezitten en de WCF cliënt proxy aanroep moet uiteindelijk ergens
uitgevoerd worden. Daarom is onderzocht of het mogelijk is om de WCF cliënt
proxy wel te blijven aanroepen, maar deze cliënt proxy ‘in te pakken’ met een
andere interface. De oplossing voor dit probleem kan uitgewerkt worden in het
wrapper design pattern (ook wel adapter genoemd).
Het wrapper design pattern wordt vaak toegepast, het is een fraaie manier om klassen samen te laten werken welke anders niet goed op elkaar aansluiten. Een nieuwe interface wordt letterlijk als inpakpapier rond de aan te roepen logica gelegd (zie figuur 2)

Figuur 2: Class diagram van wrapper design pattern

De wrapper class ondersteunt geheel of gedeeltelijk de logica van de wrapped
class maar laat zich aanroepen met zijn eigen wrapper interface. De
IWrapperInterface heeft hierbij geen enkele relatie met IWrappedInterface.
IWrapperInterface hoeft dus niet eens alle members van IWrappedInterface te
ondersteunen. Het is dus ook mogelijk om bepaalde complexiteit van het ingepakte object met het aanroepen van de wrapper te vereenvoudigen. Maar in onderstaand uitgeschreven voorbeeld houden we de twee interfaces gelijk (zie listing 2).

public class WrapperClass : IWrapperInterface
{
  private IWrappedInterface _WrappedClass;

  public WrapperClass(IWrappedInterface wrappedClass)
  {
    _WrappedClass = wrappedClass;
  }

  public string MethodOne(string parameter)
  {
    return _WrappedClass.MethodOne(parameter);
  }

  public int MethodTwo(int parameter)
  {
    return _WrappedClass.MethodTwo(parameter);
  }
}
Listing 2: een wrapper in code geschreven

Met een wrapper kan dus nog meer gedaan worden dan alleen maar het doorlussen van de methodes. Zo kan bijvoorbeeld aan iedere aanroep logging, een timer of extreme foutafhandeling toegevoegd worden.
Maar inpakken zoals in het bovenstaande voorbeeld wordt al snel monnikenwerk vanwege het uittypen, vooral indien de interface tijdens de ontwikkeling aan veel
wijzigingen onderhevig is.
Het gebruik van reflection voor dynamische code generatie biedt uitkomst om wrappers dynamisch te genereren.

Het gebruik van reflection is een uitkomst bij dynamische code generatie

Bij dit artikel zijn twee implementaties van het wrapper design pattern
meegeleverd: een versie via reflection en een versie via overerving van de
RealProxy. Beiden zullen behandeld worden.

Reflection en MSIL generatie of RealProxy

Reflection biedt de mogelijkheid om nieuwe types, compleet met methodes en
logica, runtime in het geheugen te brengen als Microsoft intermediate language
(MSIL). Dit is de ultieme just-in-time (JIT) code generatie maar in de praktijk
zie je het maar weinig zichtbaar toegepast worden. Dit komt enerzijds omdat de
veilige wereld van code editor en design-time compiler verlaten moet worden.
Anderzijds is de leercurve hoog want het komt dicht bij het zelf op de stack
zetten van attributen en het aanroepen van methodes om de stack uit te lezen en
weer verder te vullen. (kijk voor meer details op http://msdn2.microsoft.com/en-US/library/8ffc3x75(VS.80).aspx)
Wie ervaring heeft met assembler, beleeft hier het feest der herkenning. Toch is er een fundamenteel verschil met assembler. Ten eerste is er tegenwoordig
intelliSence en dat scheelt heel wat zoekwerk daar waar vaak een tekst editor
het enige gereedschap was. Maar belangrijker is dat deze MSIL wel degelijk
typesafe is en blijft! De MSIL generator zal invalide opdrachten bij het
samenstellen van het type gewoon afkeuren.
MSIL generatie is enkele factoren trager tov. het gebruik maken van voorgecompileerde code maar gelukkig kan de runtime gegenereerde code opgeslagen worden voor later hergebruik. Wrappen met MSIL heeft de voorkeur in een situatie met een hoge serverload.

Wrappen met MSIL heeft de voorkeur in een situatie met een hoge
serverload

In System.Runtime.Remoting.Proxies wordt de abstracte base class RealProxy
aangeboden, en deze geeft ook de mogelijkheid om twee interfaces op elkaar te
mappen. Alle logica rond het inpakken wordt bij de RealProxy in één
invoke methode runtime uitgevoerd, ieder methode aanroep komt hier langs.

Alle logica rond het inpakken wordt bij de RealProxy
in één invoke methode runtime uitgevoerd, ieder methode aanroep komt hier langs

Hierdoor is het echt geen vereiste meer dat de twee interfaces gelijkvormig
zijn. Voordeel van de RealProxy is dat de C# broncode eenvoudig uit te breiden
is met extra logica. Wel zal de RealProxy altijd via reflection de mapping
moeten uitvoeren en dat is relatief trager.

Runtime wrapper generatie met MSIL

Onze MSIL wrapper gaat dus runtime twee willekeurige maar ‘gelijkvormige’
interfaces op elkaar laten aansluiten. Hiervoor is een static helper class
geschreven om het gewenste type te genereren (zie listing 3).


// Step 1: Generate the wrapper class type
TypeBuilder typeBuilder =
  GenerateWrapperType(typeOfWrapperInterface);

// Remember the getter and setter
MethodBuilder methodBuilderGet;
MethodBuilder methodBuilderSet;

// Step 2: Generate the wrapped object property
GenerateWrappedObjectProperty(
  typeBuilder, typeOfWrappedInterface,
  out methodBuilderGet, out methodBuilderSet);

// Step3: Generate the constructor
GenerateConstructor(
  typeBuilder, typeOfWrappedInterface, methodBuilderSet);

// Step 4: Generate all methods
GenerateWrappedMethodes(
  typeBuilder, typeOfWrappedInterface, methodBuilderGet);

// Finally, build this wrapper type and return it
return typeBuilder.CreateType();
Listing 3: Een wrapper helper class in slechts vier stappen

De eerste stap is van administratieve aard. Types kunnen niet gegenereerd worden zonder dat er een assembly voor gedefinieerd is. Ook moet een module aanwezig zijn. Dat is geen namespace maar geeft de mogelijkheid om types te groeperen. Geef hierbij aan de typebuilder door dat een class aangemaakt moet worden en geef ook de overerving op. We gaan een overerving van de Object class met de IWrapperInterface definitie implementeren.

De tweede stap stelt een field en een property samen voor het onthouden van de
IWrappedInterface implementatie. Eerst worden de private field en de publieke
property gedefinieerd. De publieke getter en de private setter van de property
gedragen zich als methodes en die voeren code uit dus die code moet gedefinieerd worden via opcodes. Opcodes zijn de MSIL instructies waarmee bijvoorbeeld doorgegeven parameters op de stack worden geplaatst. Ook worden methode calls uitgevoerd die dan van de stack lezen en er weer op terugschrijven (zie http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes_members.aspx
voor details over de hier gebruikte opcodes).

De derde stap is het aanmaken van de constructor voor de doorgifte van
IWrappedInterface. De property met de ingepakte klas wordt zo eenmailg gevuld en daarna afgeschermd voor overschrijven.

Als laatste stap moeten alle methodes van IWrappedInterface doorgelust worden. Voor iedere te ondersteunen methode moeten alle door te geven parameters op de stack geplaatst worden en daarna wordt de methode van het ingepakte object aangeroepen om de logica uit te voeren (zie listing 4). Hiervoor maken we gebruik van de getter van de property. Een geretourneerde waarde uit de aanroep naar de methode wordt gewoon weer terug op de stack gezet.

MethodInfo[] methodInfosWrappedInterface = 
   typeOfWrappedInterface.GetMethods();

foreach (MethodInfo methodInfoInterfaceType in methodInfosWrappedInterface)
{
  //Put the parameter types in an array
  Type[] parameterTypes = new Type[methodInfoInterfaceType.
                                         GetParameters().Length];

  ParameterInfo[] pia = methodInfoInterfaceType.GetParameters();

  for (int i = 0; i < pia.Length; i++)
  {
    parameterTypes[i] = pia[i].ParameterType;
  }

  // Define the wrapper method for the wrapped method

  MethodBuilder methodBuilder = typeBuilder.DefineMethod(
      methodInfoInterfaceType.Name,
      MethodAttributes.Public
        | MethodAttributes.Virtual
        | MethodAttributes.NewSlot,
      methodInfoInterfaceType.ReturnType,
      parameterTypes);

  ILGenerator iLGenerator = methodBuilder.GetILGenerator();

  // Put 'this' on the stack
  iLGenerator.Emit(OpCodes.Ldarg_0);

  // Load the field on the stack
  iLGenerator.EmitCall(OpCodes.Call, methodBuilderGet, null);

  // Put every parameter passed on the stack

  for (int j = 0; j < pia.Length; j++)
  {
    iLGenerator.Emit(OpCodes.Ldarg, j + 1);
  }

  // Call the method of the wrapped object
  iLGenerator.Emit(OpCodes.Callvirt, methodInfoInterfaceType);

  // Ready and return the result
  iLGenerator.Emit(OpCodes.Ret);
}
Listing 4: Alle methodes overnemen en doorlussen

De MSIL generatie is afgerond, kijk nu nog eens naar Listing 3. Helemaal
onderaan na de vierde stap wordt vanuit de typebuilder het type gecreëerd en bij
MSIL generatie problemen (bijvoorbeeld onvoldoende of verkeerde parameters op de stack) zal hier een exception optreden. Als hier niet wordt geklaagd, dan hebben we nu het gewenste type van de wrapper class.
Om dit gegenereerde type te kunnen gebruiken moet de wrapper class geïnstancieerd worden waarbij de het ingepakte object aan de constructor meegegeven wordt (zie listing 5).

// Make an instance of the wrapped class
IWrappedInterface wrappedclass = new WrappedClass();

// Create the type of the wrapper
Type wrapperType = WrapperHelper.CreateWrapperType(
  typeof(IWrapperInterface),
  typeof(IWrappedInterface));

// Make an instance of the type of the wrapper
IWrapperInterface wrapper =
  (IWrapperInterface)Activator.CreateInstance(
     wrapperType, new object[] { wrappedclass });

// Call the wrapper instance
string returnValue = wrapper.MethodOne("It is a wrap!");

Console.WriteLine(returnValue);
Listing 5: Wrapper creëren en aanroepen

De gegenereerde wrapper wordt via een activator in een runtime object omgezet
welke een IWrappedInterface inpakt.

Runtime wrapper generatie met RealProxy

Het inpakken kan ook zonder MSIL generatie door gebruik van de RealProxy (zie listing 6).

public class
RealProxyWrapper<T, U> : RealProxy
{
  private U _wrappedInstance;

  public RealProxyWrapper(U wrappedInstance):base(typeof(T))
  {
    _wrappedInstance = wrappedInstance;
  }

  public override IMessage Invoke(IMessage message)
  {
    //Extract information about the method to call
    IMethodCallMessage methodCallMessage =
      new MethodCallMessageWrapper(
        (IMethodCallMessage)message);

    MethodBase methodBase = typeof(U).GetMethod(
      methodCallMessage.MethodBase.Name);

    //Invoke the method on the instance to collect result
    object returnValue = methodBase.Invoke(
      _wrappedInstance, methodCallMessage.Args);

    //Collect all information as if the method is called
    //on an instance with the wrapper interface
    ReturnMessage returnMessage = new ReturnMessage(
      returnValue, methodCallMessage.Args, methodCallMessage.ArgCount,
      methodCallMessage.LogicalCallContext, methodCallMessage);

    return returnMessage;
  }
}
Listing 6: Wrapper creëren en aanroepen

Deze wrapper zal zich voordoen als een IWrapperInterface implementatie en iedere methode aanroep wordt doorgegeven aan de via de constructor verkregen
IWrappedInterface implementatie. Het gebruikt intern de Invoke methode. Deze
doet op zijn beurt een invoke maar daar omheen kan nog extra logica
opgenomen worden zoals logging of tijdsduur metingen.

RealProxyWrapper<IWrapperInterface, IWrappedInterface>
  realProxyWrapper =
    new RealProxyWrapper
      <IWrapperInterface, IWrappedInterface>(wrappedclass);
IPlainService wrapper =
realProxyWrapper.GetTransparentProxy()
as IPlainService;

wrapper.MethodOne("It is a wrap!");
Listing 7: Wrapper creëren en aanroepen

Van de ReapProxyWrapper moet een runtime object gecreëerd worden welke op zijn beurt de wrapper levert rond het ingepakte runtime object.

Basis voor een WCF service locator

Hierboven zijn twee versies beschreven van wrappers. Met het gebruik van de
wrappers is aangetoond dat werkelijk iedere willekeurige interface definitie met
een andere interface definitie ingepakt kan worden. Deze kunnen dus ook een WCF service gaan inpakken (zie listing 8).

public interface IPlainService
{
  string DoWork();
}
…
ChannelFactory<IWcfService> factory =
  new ChannelFactory<IWcfService>(new WSHttpBinding());

IWcfService serviceToWrap =
  factory.CreateChannel(new EndpointAddress(
    "http://localhost/WcfHost/Service.svc"));

//choose between uncommenting
IPlainService wrapper = GetReflectionWrapper(serviceToWrap);
//or
IPlainService wrapper = GetProxyWrapper(serviceToWrap);

string answer = wrapper.DoWork();

factory.Close();
Listing 7: Wrapper creëren en aanroepen

We roepen hier een WCF service aan met een totaal andere interface (welke
toevallig dezelfde methode deelt). Hiermee is de mogelijkheid ontstaan om zelf
een service locator (ook wel ServiceProvider genoemd) te bouwen. Een aanroepende partij vraagt de service aan welke door een bepaalde interface type ondersteund wordt. De serviceprovider gaat hier naar op zoek, creëert de cliënt proxy en geeft deze cliënt proxy terug.

Hiermee is de mogelijkheid ontstaan om zelf een service locator te
bouwen

Conclusie

Voor mij was het bouwen van de dynamische wrappers een aangename kennismaking met MSIL. Ik heb gemerkt dat de leercurve bij MSIL eerst redelijk stijl is maar als je eenmaal bezig bent, valt alles best te begrijpen. Een goede kennis van net .Net framework is wel een vereiste. De RealProxy variant is later ontworpen en lijkt sprekend op de MSIL variant maar is veel flexibeler in het dagelijks gebruik. Deze geniet bij onze projecten dan ook de voorkeur.
Toch wil ik nog een lans breken voor MSIL generatie. Met MSIL generatie zijn heel krachtige oplossingen te bouwen die de compiler wel maar C# normaal niet toelaat binnen de taalconstructie. Zo wordt het mogelijk om bv. properties, fields en methodes die normaal private gedefinieerd zijn, toch uit te lezen. Helaas is
goede documentatie schaars maar met de juiste tools kan goed uitgezocht worden hoe bepaalde code constructies in MSIL gerepresenteerd moet worden.

Het is zeker de moeite waard om gegenereerde code eens met .Net Reflector van Lutz Roeder te openen (http://www.red-gate.com/products/reflector/) Met deze tool is het ook mogelijk om MSIL code naar andere .Net talen (C#, VB.Net, Chrome, Delphi, etc) te ‘reflecteren’. Via de plug-in techniek van .Net Reflector wordt deze lijst van talen regelmatig uitgebreid en zo is het inmiddels ook mogelijk om ter controle MSIL code om te zetten naar broncode met de
ReflectionEmitLanguage plug-in.

Veel succes!

Deze blog is een aangepaste versie van een artikel in de SDN Magazine nr. 99 (nov. 2008) Zie ook http://www.sdn.nl/

Advertenties

Web.Config Slice-And-Dice

De web.config is de standaard oplossing voor het configureren van de asp.net
websites. Zowel instellingen van ASP.Net zelf als door de ontwikkelaar gekozen
configuratie-instellingen kunnen daarin opgeslagen worden.

Standaard wordt de web.config bijgewerkt door de ontwikkelaar waarna bij het opleveren van de website, door middel van een installatiehandleiding, aangegeven wordt welke instellingen achteraf nog aangepast moeten worden zoals afwijkende paden voor bestanden of ander connectionstrings.

Eigenlijk is dit pesten van systeembeheers. Iedere ontwikkelaar die wel eens in de web.config heeft gekeken, weet dat er ontzettend veel risico’s aan het wijzigen van configuratiebestanden hangen. Zelf hebben we ook een hekel aan die onleesbare XML oprisping. Een onbedoelde verminking is zo gemaakt en dan gaat de site gewoon op zwart met de meest uiteenlopende, vaak nietszeggende foutmeldingen.

Toch is dit risico redelijk te beperken zonder extra moeite voor de ontwikkelaar. Sterker nog, ook voor hen is er een voordeeltje te halen. Het is namelijk minder bekend dat we als een japanse chefkok de web.config gewoon in aparte bestanden kunnen opsnijden zodat de lengte ervan aanzienlijk vermindert.

Stel, je hebt een web.config:

<?xml version="1.0"?>
<configuration>
  . . . . .
  <appSettings>
    <add key="sleutel" value="tekst" />
  </appSettings>
</configuration>

In dit voorbeeld kun je de appSettings vast wel terugvinden maar een gemiddelde web.config zal eerder enkele honderden regels aan XML elementen
bevatten en dat is een stuk minder overzichtelijk.

De oplossing is dan om de application settings sectie te verhuizen naar een
apart bestand en deze te benoemen als config source in de oorspronkelijke
web.config sectie:

<?xml version="1.0"?>
<configuration>
  . . . . .
  <appSettings configSource="config\web.appsettings.config" />
</configuration>

Hier is het aparte bestand met de veelzeggende naam ‘web.appsettings.config’
in een map, genaamd ‘config’, geplaatst. In dat bestand staat dan de uiteindelijke appSettings sectie, dus zonder de configuration omarming etc.

<appSettings>
  <add key="sleutel" value="tekst" />
</appSettings>

Bij het starten van de website wordt de web.config samengevoegd met alle
gerefereerde bestanden. Er is dus voor de rest geen verschil tussen het
originele configuratiebestand en de combinatie van al die configuratiebestanden.
Sterker nog, tooling zoals de Asp.NET Configuration website ( Menu | Website |
ASP.NET Configuration) blijven gewoon werken met deze situatie en schrijven ook gewoon in de aparte bestanden.

Secties kunnen ook versleuteld worden zodat de inhoud niet meer te lezen is.
De inhoud van een sectie wordt in de web.config gesleuteld. Kijk voor een prima
uitleg op de site van 4 guys from Rolla. Dit werkt dus ook nog steeds bij een sectie in een apart bestand. Hier is een voorbeeld van de connectionstring sectie buiten de web.config:

<connectionStrings configProtectionProvider="
                             DataProtectionConfigurationProvider">
  <EncryptedData>
    <CipherData>
      <CipherValue>AQAAAN [knip] tVZEu</CipherValue>
    </CipherData>
  </EncryptedData>
</connectionStrings>

Een dergelijke versleuteling is dus perfect als in de connectionstring bv.
gevoelige informatie zoals een inlognaam of wachtwoord staat. Deze informatie is dus niet meer leesbaar hoewel ontsleutelen wel mogelijk blijft, afhankelijk van
de gekozen versleutelmethode. Bijkomstig voordeel is dat hier dus simpel
bestanden vervangen kunnen worden, bv. bij gebruik van een machine afhankelijke versleuteling.

De tweede sectie en volgende secties moeten allen in apart bestanden
ondergebracht worden. Verschillende secties kunnen niet samen ondergebracht
worden in een enkel bestand. Dit kan je tot de verleiding brengen om alle
mogelijke secties te verhuizen naar aparte bestanden.

Ik heb hier de complete web.config van een nieuw Asp.NET project
teruggebracht tot nog maar enkele regels XML door secties te verplaatsen. De
overige heb ik verwijderd en de applicatie start nog gewoon op:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation configSource="config\web.compilation.config" />
    <authentication configSource="config\web.authentication.config" />
    <customErrors configSource="config\web.customErrors.config" />
    <pages configSource="config\web.pages.config" />
    <httpHandlers configSource="config\web.httpHandlers.config" />
    <httpModules configSource="config\web.httpModules.config" />
  </system.web>
  <system.codedom configSource="config\web.system.codedom.config" />
  <appSettings configSource="config\web.appSettings.config" />
  <connectionStrings configSource="config\web.connectionStrings.config" />
</configuration>

Niet iedere sectie kan zondermeer verhuisd worden maar dit ruimt wel lekker op 🙂

Nu kun je dus de keuze maken om werkelijk alle secties in aparte bestanden onder te brengen zodat eventuele wijzigingen verdeeld worden over verschillende bestanden of om allen statische instellingen te verhuizen en de
wijzigingen te verzamelen in de web.config. De laatste optie spreekt mij het
meeste aan. Het ging er aan het begin van dit verhaal vooral om dat
systeembeheer tegen zichzelf beschermd kan worden en dat de te onderhouden
instellingen weer overzichtelijk worden. Laat de te onderhouden settings dus
gewoon in de web.config en verhuis de rest.