Benyt Scriptmanager til at kombinere scriptfiler

Jeg har længe søgt efter en metode til, at optimere mængden af scripts på en side, der er implementeret i WebForms (ASP.NET 3.5 SP1). Idag fandt jeg et bud på en løsning, omend ikke helt optimal, så hjælper den da med at få samlet scripts i potentielt langt færrere resurser.

Script Manger samler scripts

Der findes faktisk allerede en metode på siden, så det er bare med at gribe og udnytte den. 

Hvis udgangspunktet er flg. udsnit af script-tags:

  <script src="/scripts/jquery.min.js"></script>
  <script src="/scripts/jquery.autocomplete-min.js"></script>
  <script src="/scripts/jquery.innerfade.js"></script>
  <script src="/scripts/jquery.cookie.js"></script>
  <script src="/scripts/common.js"></script>
  <script src="/scripts/specific-1.js"></script>
  <script src="/scripts/specific-2.js"></script>
  <script src="/scripts/specific-3.js"></script>

så kan dette omsættes til en scriptresurse således:

  
<asp:ScriptManager runat="server">
  <CompositeScript>
    <Scripts>
      <asp:ScriptReference Path="/scripts/jquery.min.js">
      </asp:ScriptReference>
      <asp:ScriptReference Path="/scripts/jquery.autocomplete-min.js">
      </asp:ScriptReference>
      <asp:ScriptReference path="/scripts/jquery.innerfade.js">
      </asp:ScriptReference>
      <asp:ScriptReference path="/scripts/jquery.cookie.js">
      </asp:ScriptReference>
      <asp:ScriptReference path="/scripts/common.js">
      </asp:ScriptReference>
      <asp:ScriptReference Path="/scripts/specific-1.js">
      </asp:ScriptReference>
      <asp:ScriptReference Path="/scripts/specific-2.js">
      </asp:ScriptReference>
      <asp:ScriptReference Path="/scripts/specific-3.js">
      </asp:ScriptReference>
    </Scripts>
  </CompositeScript>
</asp:ScriptManager>

Indbyggede scriptresurser kan også kombineres (og endda automatisk "minificeres"). I dette tilfælde skal disse resurser blot tilføjes i scriptmanagerens Composite-underelement med de korrekte referencer

  
<asp:ScriptManager runat="server" ScriptMode="Release">
  <CompositeScript>
    <Scripts>
      <asp:ScriptReference Path="/scripts/jquery.min.js">
      </asp:ScriptReference>
      <asp:ScriptReference Path="/scripts/jquery.autocomplete-min.js">
      </asp:ScriptReference>
      <asp:ScriptReference path="/scripts/jquery.innerfade.js">
      </asp:ScriptReference>
      <asp:ScriptReference path="/scripts/jquery.cookie.js">
      </asp:ScriptReference>
      <asp:ScriptReference path="/scripts/common.js">
      </asp:ScriptReference>
      <asp:ScriptReference Path="/scripts/specific-1.js">
      </asp:ScriptReference>
      <asp:ScriptReference Path="/scripts/specific-2.js">
      </asp:ScriptReference>
      <asp:ScriptReference Path="/scripts/specific-3.js">
      </asp:ScriptReference>
      <asp:scriptreference name="MicrosoftAjax.js" 
        assembly="System.Web.Extensions, Version=3.5.0.0, 
                Culture=neutral, PublicKeyToken=31BF3856AD364E35">
      </asp:ScriptReference>
      <asp:scriptreference name="MicrosoftAjaxWebForms.js" 
        assembly="System.Web.Extensions, Version=3.5.0.0, 
                Culture=neutral, PublicKeyToken=31BF3856AD364E35">
      </asp:ScriptReference>
    </Scripts>
  </CompositeScript>
</asp:ScriptManager>

Ovenstående brug af Script Manager resulterer i, at én ScriptResource.axd hentes i stedet for 10, som det ville have været i dette eksempel.

Hvis man vil have sine scripts mnified, skal dette dog stadig gøres manuelt og disse minified scriptfiler skal refereres i Script Manageren. Dog vil ScriptMode="Release" medføre, at MicrosoftAjax og MicrosoftAjaxWebForms minificeres automatisk.

Andre kilder

Der kan findes mere uddybende og vidtgående information i artiklen "ASP.NET AJAX - Script Combining and moving ScriptResource.axd's to Static Scripts"  (af selveste Scott Hanselman).

Generisk Repository med LINQ to SQL

I mit arbejde med at udvikle systemer har jeg indtil videre håndkodet repositories, altså integration til databasen via en klasse, der skal sørge for at afskærme resten af applikationen fra databasevalget. Dette har som sådan fungerert OK, men det er, for en stor del, en triviel opgave og derfor kedeligt i længden. Det ville altså være rart kun, at skulle kode samme logik én gang, altså følge DRY-princippet.

Jeg er i forbindelse med, at jeg er dykket ned i EF 5 og Code First, blevet introduceret for et generisk repository, hvilket har åbnet mine øjne for mulighederne i Generic (som efterhånden har nogle år på bagen i .NET). Dog har jeg ikke mulighed for at benytte EF 5 i mine gamle projekter, dels fordi disse i høj grad baserer sig på Linq to SQL, dels fordi disse er implementeret i .NET 3.5. Derfor jeg har søgt efter et alternativ, som kunne bruges ifm. det miljø... og når man søger, skal man som bekendt finde (om ikke andet inspiration), hvilket jeg gjorde i denne artikel: "A generic implementation of the Repository Pattern for LINQ to SQL"

Jeg vil ikke påstå jeg forstår koden fuldt ud, men jeg kan dog godt se de smarte i, at jeg kan smide et hvilket som helst Entity-objekt ned i halsen på repositoryklassen og få udført de relevante handlinger på dette. Såvidt jeg kan se, skal forretningsobjekterne implementere et interface for at dette kan fungere. Dette er jeg som udgangspunkt ikke specielt interesseret i, da jeg gerne vil have adskilt mine repositories helt fra forretningsobjekterne (som man kan med EF + mapping-klasser + fluent API til definering af dataskema).

Derfor har jeg et alternativt forslag til, hvordan man kan opnå dette. Det kræver dog, at man implementerer lidt ekstra kode mhp. mapning mellem database-entities og forretningsobjekter. Jeg har valgt at implementere denne mapning af data bag et interface, som det generiske repository kan gøre brug af (strategy pattern). Mere om det senere i artiklen.

Først til udgangspunktet, nemlig forretningsobjekterne.

Forretningsobjekterne

Dette er i min verden logikken omkring forretningen, dvs. her skal ikke være nogen kendskab til hvordan objektet materialiseres eller gemmes, men kun regler og data vedr. objektet.

Et "rent" forretningsobjekt kunne f.eks. se således ud:

public class Person
{
   public int Id { get; set; }
   public string Navn { get; set; }
   public string Email { get; set; }
}

Det kan så diskuteres om ikke Id er en databasespecifik egenskab, men her vælger jeg så ikke at blive alt for puristisk (til trods for det jo var et af argumenterne for, at lave den løsning jeg her præsenterer :-)). Id er relevant andre steder i systemet, da det er en bekvem måde at identificere objektets data på.

Transport af data til databasen

Jeg vil gerne have mine data gemt i en database, så jeg kan arbejde videre med dem på et senere tidspunkt, så derfor har jeg lavet en tabel i min database, som ser nogenlunde således ud:

person = 
    id int
 + navn nvarchar(50)
 + email nvarchar(250)

Når dette kværnes igennem LINQ To SQL's ORM i Visual Studio, resulterer det i et objekt, der i korte træk ser nogenlunde således ud:

public partial class person
{
    public int id { get; set; }
    public string navn { get; set; }
    public string email { get; set; }
}

Nu bliver udfordringen så, at få mit forretningsobjekt transporteret frem og tilbage til hhv. fra databasen. Dertil har jeg mit repositoryobjekt.

Repositoryklasserne

Normalt ville jeg implementere en klasse med flg. struktur:

public class PersonRepository
{
    public Person Get(Person obj) { }
    public IEnumerable<Person> FindAll(
            FindPersonKriterier kriterier
        ) {}
    public IEnumerable<Person> Find(
            out int total, 
            FindPersonKriterier kriterier
        ) {}
    public Person Create(Person obj) { }
    public Person Update(Person obj) { }
    public Pesron Delete(Person obj) { }
}

Det er så her jeg udvikler min dovenskab lidt, for sådan et skulle jeg jo i så fald lave for hvert eneste forretningsobjekt (og har gjort det indtil videre), hvilket som sagt er noget omstændigt.

Ud over jeg skulle implementere denne klasse for hver forretningsobjekt, skulle jeg også implementere en FindForretningsobjektKriterier, der skulle holde på alle mine søgekriterier for den pågældende klasse.

Generics redder dagen

Derfor vil jeg i stedet kaste mig ud i Generics og flikke en respositoryklasse sammen, som kan bruges i (næsten) alle sammenhænge. Den kunne se således ud:

public class GenericRepository<TEntity, TDoamin>
{
    private readonly dataContext _context;
    private readonly IDomainDBMapper<TEntity, TDomain> _mapper;

    public GenericRepository(
            dataContext db,
            IDomainDBMapper<TEntity, TDomain> mapper
        )
    {
        _context = db;
        _mapper = mapper;
    }

    private Table<TEntity> GetTable
    {
        get { return _context.GetTable<TEntity>; }
    }

    public TDomain Get(
            Expression<Func<TEntity, bool>> idFilter
        ) 
    {
        var dbo = GetTable.SingleOrDefault(idFilter);
        return _mapper.DbToObject(dbo, null);
    }


    public IEnumerable<TDomain> FindAll(
            Expression<Func<TEntity, bool>> filter,
            Func<IQueryable<TEntity>, 
                     IOrderedQueryable<TEntity>> orderBy
        )
    {
        var total = 0;
        return Find(out total, filter, orderBy, 0, 1);
    }

    public IEnumerable<TDomain> Find(
            out int total,
            Expression<Func<TEntity, bool>> filter,
            Func<IQueryable<TEntity>, 
                     IOrderedQueryable<TEntity>> orderBy
            int pageSize,
            int page 
        )
    {
        IQueryable<TEntity> res = GetTable;
            
        if(filter != null)
            res = res.Where(filter);

        if (orderBy != null)
            res = orderBy(res);

        total = res.Count();

        if(pageSize > 0)
        {
            if(page > 1)
                res = res.Skip((page - 1) * pageSize);
            res = res.Take(pageSize);
        }

        return
            (
                from q in res
                select mapper.DBToDomain(q, null)
            ).ToList<TDomain>();
    }

    public TEntity Create(TDomain obj) 
    {
        var dbObj = CreateEntity(obj);
        GetTable.InsertOnSubmit(dbObj);
        return dbObj;
    }

    public TEntity Update(
            TDomain obj, 
            Expression<Func<TEntity, bool>> idFilter
        ) 
    {
        var dbObj = GetTable.SingleOrDefault(idFilter); 

        if(dbObj == null) 
            throw new NullReferenceException(); 

        mapper.ObjectToDB(obj, dbObj); 

        return dbObj;
    }

    public TEntity Delete(Expression<Func<TEntity, bool>> idFilter) 
    {
        var dbObj = GetTable.SingleOrDefault(idFilter); 

        if(dbObj != null) 
            GetTable.DeleteOnSubmit((TEntity)dbObj); 

        return dbObj; 
    }

    public TEntity CreateEntity(TDomain obj)
    {
        var dbObj = CreateInstance<TEntity>();
        mapper.ObjectToDB(obj, dbObj);
        return dbObj;
    }

    protected T CreateInstance<T>()
    {
        return Activator.CreateInstance<T>();
    }
}

Klassen holder en reference til datakonteksten og kender som sådan ikke noget til processen at oprette dette objekt. Der foretages heller ikke nogen commit (SubmitChanges) i repositoryklassen, da dette skal ske hos den kaldende part (f.eks. Unit of Work). Repositoryklassen ved, som udgangspunkt, ikke, hvornår klienten er klar til at committe sig til de foretagne ændringer. Se senere i afsnittet om Unit of Work-klassen for at se, hvordan det kan anvendes i praksis.

Insert/Update/Delete

Disse metoder tager sig helt basalt af, at oprette, opdatere og slette data.

For såvidt angår Update og Delete, medsendes der, af den kaldende part, et lambda-udtryk, som specificerer hvordan repository-klassen skal identificerer den række der skal opdateres hhv. slettes.

Update og Delete har brug for at kunne identificere den række der skal opdateres hhv. slettes og dette sker ved at den kaldende part medsender et lambda-udtryk, som angiver hvordan rækken findes.

Get

Henter en given række i databasen baseret på rækkens id-kolonne. Id'et findes ved at den kaldende part medsender et lambda-udtryk, der specificerer hvordan repository-klassen skal identificere rækken der skal hentes.

FindAll/Find

Disse to metoder finder frem til data på grundlag af et filter (som også er et lambda-udtryk). Dette filter kan som sådan være mere eller mindre komplekst. Pointen her er, at man filtrerer direkte i databasen, dvs. transporten af data er minimal og dermed godt for både netværk og hukommelsesforbruget.

Find stiller yderligere muligheden for, at paginere resultatet, til rådighed. Dette sker ligeledes direkte i databasen og fremmer dermed ovenstående pointe yderligere. Desuden returnerer Find også det totale antal poster fundet ved søgningen, sammen med det paginerede resultat. Således har man mulighed for at foretage yderligere tilpasning ifm. præsentationen af data (f.eks. lave en "pager").

Udover filtrering og paginering, giver begge metoder mulighed for, at sortere resultatet. Dette sker ligeledes i databasen. På samme måde som med filteret, sendes sorteringsbetingelserne også med som et lambda-udtryk.

CreateEntity

Metoden benyttes til at skabe et database entity-objekt, der afspejler forretningsobjektet. Dette kan bruges i sammenhænge, hvor man har brug for at splitte indsættelsesprocessen op i flere trin. Se eksempel herpå i forbindelse med Ordre + Ordrelinjer sidst i artiklen.

Støttemetoder

Ud over det faktiske interface, er der metoder til, at oprette en instans af forretningsobjekter og databaseobjekter (CreateInstance), samt en metode til, at hente table-klassen for det givne databaseobjekt (GetTable). Disse er behagelighedsmetoder og kunne som sådan godt undværes. De gør bare resten af koden lettere at læse.

Som det kan ses, er den generiske respositoryklasse ikke i sig selv afskærmende i forhold til omverdenen, dvs. Linq To SQL's entiteter kendes også af den kaldende part. Min tanke er dog, at Unit of Work-klassen skal være denne afskærmning, således Unit of Work-klassen kun kommunikerer forretningsobjekter i sit interface, men internt er næd til at arbejde med og kende til databaseobjekterne. Dette er nødvendigt for at Unit of Work-klssen kan foretage handlinger på tværs af flere forskellige repositories i én transaktion. Læs mere om denne konstruktion senere i artiklen.

Udfordringen herfra er nu, at få data transporteret fra det ene format (forretningsobjektet) over i det andet (databasenobjektet) og til dette formål skal jeg bruge en mapper-klasse

Mapperklassen som adapter

Da der er tale om et generisk repository, kendes de typer der arbejdes med, ikke på forhånd, så repositoryklassen kan ikke selv stå for mapning af data (med mindre man kaster sig over Reflection). Derfor uddelegeres dette arbejde til en mapperklasse. Denne mapperklasser implementerer et interface, som repositoryklassen kan få lov til at kende på compile tidspunktet, nemlig IDomainDBMapper. Dette interface definerer to metoder, en der kopierer data fra databaseklassen til forretningsobjektet og en der kopierer data den anden vej. Det kunne se således ud:

public interface IDomainDBMapper<TEntity, TDomain> 
{ 
    TEntity DomainToDB(TDomain, TEntity); 
    TDomain DBToDomain(TEntity, TDomain); 
}

Den specifikke mapperklasse implementerer, som nævnt, interfacet ovenfor og kan se således ud:

public class PersonMapper 
        : IDomainDBMapper<person, Person>
{
    public Person DBToDomain(
            person dbObj, 
            Person obj
        )
    {
        if(obj == null)
            obj = Activator.CreateInstance<Person>();

        obj.Id = dbObj.id;
        obj.Navn = dbObj.navn;
        obj.Email = dbObj.email;

        return obj;
    }

    public void DomainToDB(
            Person obj, 
            person dbObj
        )
    {
        dbObj.navn = obj.Navn;
        dbObj.email = obj.Email; 

        return dbObj;
    }
}

Som det ses, er dette en relativ simpel klasse, som burde være let at teste og som ikke burde give de store problemer at implementere.

Herfra kunne man faktisk godt gå igang med at bruge det generiske repository, men det ville med nogen sandsynlighed blive et sammenfiltret system med bindinger langt ned i databaselaget helt oppe fra GUI. Dette er ikke ønskeværdigt, så derfor bør det overvejes, om repository-klasserne skal pakkes ind. Det kan f.eks. gøres ved at lave et lag, som håndterer alt logik der omhandler lagring og læsning af data. Ind fra højre kommer Unit of Work...

Unit of Work-klassen (UoW)

Denne klasse pakker repositoryklassen (eller -klasserne) ind og er, som sådan, et transaktionslag mellem forretningsobjekterne og databasen via en række handlinger, der sørger for at udføre specifikke handlinger med data. Disse handlinger kunne f.eks. være at hente en liste af et forretningsobjekt, eller gemme en samling af objekter af samme eller forskellig type. UoW-klassen definerer det antal repository-klasser der er behov for, for at håndtere de opgaver der skal løses. Det kunne f.eks. se således ud:

public class PersonUnitOfWork : IDisposable
{
    private readonly dataContext _context;
    private readonly GenericRepository<person,Person> _repository;

    public PersonUnitOfWork()
    {
        _context = new dataContext();
        _repository = 
            new GenericRepository<person,Person>(
                _context,
                new PersonMapper()
            );
    }

    public void Dispose()
    {
        _context.Dispose();
    }

    public List<Person> SoegPaaNavn(string navn)
    {
        var lowerNavn = navn.ToLower();
        return _repository.FindAll(
                o => o.navn.ToLower().Contains(lowerNavn), 
                q => q.OrderBy(ob => ob.navn)
            ).ToList();
    }

    public Person Hent(int id)
    {
        return _repository.Get(o => o.id == id);
    }

    public void Gem(Person obj)
    {
        person dbObj = null;
        var isNew = obj.Id == 0;

        if(isNew)
            dbObj = _repository.Insert(obj);
        else
            dbObj = _repository.Update(obj, o => o.id == obj.Id);

        _context.SubmitChanges();

        if(isNew)
            obj.Id = dbObj.id;
    }

    public void Slet(Person obj)
    {
        _repository.Delete(o => o.id == obj.Id);
        _context.SubmitChanges();
        obj.Id = 0;
    }
}

Ovenstående UoW-klasse er helt simpel, men den viser alligevel nogle af de elementer man skal arbejde med i forbindelse med det generiske respository. Desuden kan man "pakke den ned" sammen med resten af databaselaget, da interfacet til databasen hermed er afskærmet, idet der kun arbejdes med forretningsobjekter udadtil.

Jeg er klar over det kan være fristende, at lægge forretningslogik i repository-laget, men UoW-klassen skal opfattes, som en adapter mellem databasen og forretningsobjekterne. Derfor skal UoW-objekterne kun håndtere den logik der omhandler, at fordele data ud i de rigtige repositories, alt andet logik skal ligge i forretningsobjektet eller et separat serviceobjekt.

Anvendelse af UoW i praksis

UoW-klassen kan bruges således:

using(var uow = new PersonUnitOfWork())
{
    var personListe = uow.SoegPaaNavn("Hansen");
    repeaterPersoner.DataSource = personListe;
    repeaterPersoner.DataBind();
}

Her søges der efter alle personer, hvor efternavnet Hansen er en del af deres navn. Resultatet bindes til en repeater, der forventes, at findes på siden.

Bemærk, at UoW implementerer IDisposable for at kunne frigive resurser ifm. Garbage Collection. Derfor vælger jeg også at benytte mig at using-konstruktionen ifm. brugen af UoW.

Mere komplekse anvendelsesområder for UoW

Eksemplerne i denne artikel er noget simple, men Unit of Work kommer for alvor til sin ret, når man skal håndtere større objekthierarkier, hvor data skal gemmes som et samlet hele (dvs. en transaktion), idet man med den illustrerede UoW-struktur, kan opdatere flere repositories inden SubmitChanges kaldes på DataContext-objektet. Det er værd at bide mærke i, at dataContext-objektet styres af UoW-klassen og at det dermed er her beslutningen om kald af SubmitChanges tages.

Eksempelvis kunne en ordre i et shopsystem, der typisk kan indeholde flere ordrelinjer, gemmes ved først at oprette en ordre i ordretabellen, og derefter tilføje ordrelinjer i ordrelinjetabellen. Til sidst kan SubmitChanges kaldes for at effektuere selve lagringen.

public class OrdreUnitOfWork : IDisposable
{
    private readonly dataContext _context;
    private readonly 
         GenericRepository<ordre,Ordre> _ordreRepository;
    private readonly 
         GenericRepository<ordrelinje,Ordrelinje>
             _ordrelinjeRepository;

    public OrdreUnitOfWork()
    {
        _context = new dataContext();
        _ordreRepository = 
            new GenericRepository<ordre,Ordre>(
                _context,
                new OrdreMapper()
            );
        _ordrelinjeRepository = 
            new GenericRepository<ordrelinje,Ordrelinje>(
                _context,
                new OrdrelinjeMapper()
            );
    }

    // øvrige metoder udeladt...

    public void Gem(Ordre obj)
    {
        if (obj.Id == 0)
        {
            var dbObj = _ordreRepository.Insert(obj);
            foreach(var ol in obj.Ordrelinjer)
            {
                dbObj.ordrelinje.Add(
                    _ordrelinjeRepository.CreateEntity(ol));
            }
            _context.SubmitChanges();
            if(dbObj != null)
                obj.Id = dbObj.id;
        }
        else
        {
            _repository.Update(obj, o => o.id == obj.Id);
            foreach(var ol in obj.Ordrelinjer)
            {
                if(ol.Id == 0)
                    dbObj.ordrelinje.Add(
                        _ordrelinjeRepository.CreateEntity(ol));
                else
                    _ordrelinjeRepository.Update(ol); 
            }
            _context.SubmitChanges();
        }
    }

    // øvrige metoder udeladt...
}

Som det ses, håndteres ordrelinjer specielt, da disse skal gemmes i den rigtige rækkefølge ifht. ordren (ordren skal først oprettes og have et id tildelt, før ordrelinjerne kan relateres til ordren). Dette klarer Linq To SQL for os, hvis blot ordrelinjerne tilknyttes på den korrekte måde, dvs. via Add-metoden på dbOrdre's ordrelinje-liste.

Håndteringen af ordrelinjer kunne i princippet også håndteres i mapperklassen, hvis der er lavet en relation mellem ordren og ordrelinjerne i databaseskemaet (hvilket der jo nok typisk er).

Jeg håber dette kan være med til, at hjælpe dig på vej til mere overkommeligt liv med repository-pattern og forretningsobjekter. Smid gerne en kommentar, hvis du har forslag til forbedringer eller sprøgsmål til implementeringen.

Dynamisk opdatering af resursefiler i ASP.NET

I forbindese med at jeg skal lave et site der understøtter flere sprog, har jeg kastet mig ud i et forsøg på at bruge resursefiler til at styre sprog/tekster.

Dette er jo oplagt, da resurserne er let tilgængelige i koden og i views. Desuden er der en indbygget integration mellem MVC-frameworkets DataAnnotations og resursefilerne, som ligeledes er let tilgængelig.

Udfordringer

Alt er, indtil videre, fint og sprogstyringen kører som sådan smertefrit. Jeg har dog behov for at kunne vedligeholde teksterne via mit administrationssystem og dermed mindst at kunne opdatere eksisterende tekster uden at skulle bygge min applikation og uploade igen.

Uden de store problemer formår jeg, dynamisk, at få indlæst de forskellige sprogversioner af resursefilerne og få dem vist i mit administrationssystem. Men så opstår udfordringerne, idet jeg jo gerne vil gemme mine ændringer i resursefilerne igen. Selvom det også går relativt smertefrit, at gemme mine ændringer i resx-filerne, opstår der i den forbindelse dog lige et lille problem: Mit site holder op med at svare. Frustrerende!

Det viser sig så, at når resx-filen opdateres, så bygger sitet automatisk systemet igen og dermed bliver sitet "lagt ned" i den periode dette sker. I mit tilfælde, hvor jeg kører en Azure Emulator, lader det ikke til at sitet kommer på benene igen overhovedet, men det er ikke noget jeg er helt sikker på (manglende tålmodighed til at vente længe nok).

Denne genstart medfører desuden, at alt hvad der hedder session-data slettes, fordi applikationen genstartes. Dette kan være ret uhensigtsmæssig - med mindre der er opsat distribueret caching af session, eller man slet ikke benytter session-state (som mig) ;-)

En delvis løsning

Det kan tilsyneladende ikke lade sig gøre at undgå denne bygning af koden, med mindre man går på kompromis med nogle af de integrationer til resursefiler, der er rundt omkring i frameworket. Desværre har jeg ikke fundet en løsning der er 100% optimal, men noget der måske kunne bruges er "Updatable Resources", som omgår de File Change Notifications, som medfører af applikationen bygges ved ændringer i f.eks. resx-filerne.

Jeg er klar over, at artiklen er noget gammel (2009), men jeg har ikke kunnet finde noget information, der var mere up to date, ej heller nogle der omhandlede MVC 4, som jeg pt. arbejder i.

Jeg har fået opfattelsen af, at det ikke er muligt at lave sin egen resursefabrik, således den smertefrit integrerer med MVC-frameworkets DataAnnotation, på samme måde, som den indbyggede. Det undrer mig bare, at dette ikke skulle være muligt. Det er muligt, at dette problem ikke eksisterer længere (der er trods alt løbet meget vand under broen siden 2009!). Umiddelbart ville jeg jo forvente, at resurser, ligesom så mange andre elementer i .NET, var pluggable og dermed kunne udskiftes efter behov. Spørgsmålet er bare hvor man skal proppe sine ændringer ind...(?)

Se også

Rick Strahl's gennemgang af en hjemmestrikket ResourceProvider der henter resurserne fra en database:  http://www.west-wind.com/presentations/wwdbResourceProvider/

Parameteriserede tekstmønstre

I mit arbejde med websites har jeg til tider haft behov for at kunne vise tekster overfor brugeren, hvor der indgik værdier eller kvantificeringer. Dette har ofte indvirkning på formen af teksten (entals- kontra flertalsformer). Dette kan give en udfordring med antallet af tekster man skal vedligeholde, alternativt den logik der skal indarbejdes i siderne, for at håndtere dette.

Derfor har jeg (inspireret af en artikel jeg, for længe siden, læste om emnet, og desværre ikke kan finde igen), udarbejdet et regelsæt for hvordan man kan styre de mest almindelige formændringer i tekst ved simpelthen at definere et tekstmønster i stedet for den endelige tekst.

Formatet

tal := {0-9}1-n

felt := "{" + <tal> + "}"

operator := "!" | "=" | "<" | ">"

! (operator) := forskellig fra

= (operator) := lig med

< (operator) := mindre end

> (operator) := større end

tekst := et eller flere karakterer undtagen ":"

værdi := <tal>

mønster := "[#" + <tal> + <operator> + <værdi> + ":" + <tekst> + {"|" + <operator> + <værdi> + ":" + <tekst>} + "]"

Eksempel:

"Der blev fundet {0} sag[#0!1:er] i {1} mappe[#1!1:r]"

Hvor 1 fundet sag i 1 mappe vil resultere i flg. tekst:

"Der blev fundet 1 sag i 1 mappe"

Med 2 fundne sager i 1 mappe vil teksten se således ud:

"Der blev fundet 2 sager i 1 mappe"

og så fremdeles...

Konkretisering af konceptet

Med denne regel kan man så påbegynde implementeringen af en funktion til at håndtere dette. Jeg har lavet et par implementering, dels i JavaScript, dels i C#.

I C#-versionen har jeg så lavet et par extension-metoder til string-objektet, således man kan ændre formen på en given streng. Metoden kalder jeg Parameters og den har formatet:

public static class MyExtensions
{
  public static string Parameters(
      this string val, 
      params object[] args
    )
  {
    var parser = new PatternParser();
    return String.Format(parser.Parse(val, args), args);
  }

  public static int ToInt(this string val)
  {
    var i = 0;
    int.TryParse(val, out i);
    return i;
  }

  public static bool IsNumeric(this string val)
  {
    var i = 0;
    return int.TryParse(val, out i);
  }
}

Der er også defineret 2 andre extension-metoder til strings, som benyttes i koden nedenfor.

Eksempel:

"{0} sag[#0!1:er] i {1} mappe[#1!1:r]".Parameters(1,2)

Parametrene kan naturligvis være variable og strengen kan f.eks. komme fra en resursefil, eller en database.

Parseren

Implementeringen af parseren ser således ud:

public class PatternParser
{
  private static readonly Regex re = 
      new Regex(@"\[\#(\d+)((([\=\!\])(.*?)\:(.*?))(?:\|*))*\]");

  public string Parse(string text, object[] parameters)
  {
    return re.Replace(text, m =>
    {
       var fieldIndex = int.Parse(m.Groups[1].Captures[0].Value);
       var fieldValue = int.Parse((string)parameters[fieldIndex]);

       for (int c = 0; c < m.Groups[2].Captures.Count; c++)
       {
          var compareValue = int.Parse(m.Groups[5].Captures[c].Value);

          if (Comparer(
                fieldValue, 
                m.Groups[4].Captures[c].Value, 
                compareValue))
            return m.Groups[6].Captures[c].Value;
       }
       return "";
    });
  }

  private bool Comparer(int a, string operatorValue, int b)
  {
    switch (operatorValue)
    {
      case "=": return a == b;
      case "!": return a != b;
      case "<": return a < b;
      case ">": return a > b;
    }

    return false;
  }
}

Den kan med stor sandsynlighed gøres meget fixere, men betragt denne udgave som et proof of concept og leg selv med optimeringen :-)

Hvad kan man så bruge dette til?

Som nævnt kan man bruge det til at styre former på tekster og det kan med fordel bruges ifm. sprogstyring af et system, dvs. hvor et system skal præsenteres i flere forskellige sprog. Dette er interessant, da sprog har forskellige ordstillinger ligesom der er stor forskel på, hvordan entals- og flertalsendelser styres. Nogle sprog har specifikke endelser for 1, 2, 3, 4, 5 af en ting, hvor vi i det danske sprog kun skelner mellem ental og flertal. Disse regler kan hjælpe med at styre disse tekster uden at ændre på det input strengen skal have der hvor teksten skal præsenteres.

Se også

Jeg har leget lidt med tankerne på Eksperten.dk, som er sammenfattet i en guide (dog med udgangspunkt i flersprogstrying).

Min MVC sikkerhedscheckliste

Jeg har flg. chekliste, som jeg bruger ifm. udvilking af systemer der baserer sig på ASP.NET MVC. En del af listens punkter kan også overføres på andre platforme og frameworks.

Listen er en foreløbig liste, dvs. der kan (og vil givetvis) kommer opdateringer til den, men dette er altså hvad jeg har pt.

Autorisation

  • Alle sider (controller actions) der skal være beskyttet, har en Authorize atribut. Alternativt er der opsat et globalt filter, således alle sider er beskyttet.
  • Alle sider der har Authorize-atribut tilknyttet leveres over https (SSL). Der må IKKE være indhold på siden, som kommer fra en "untrusted" kilde. Der må helst ikke være noget indhold overhovedet der leveres over http på disse sider.
  • Loginsiden leveres ligeledes, i sin helhed, over https (dvs. ingen resurser på siden må leveres over http).
  • Cookies der har noget med brugeridentificering eller på anden måde indeholder følsomme data, leveres kun over https (dvs. secure er sat på cookien).
  • Cookies i øvrigt, leveres kun over httpOnly, med mindre der skal arbejdes med cookies på klienten.

XSS (Cross Site Scripting)

  • Alle informationer der vises i sider er encoded (HTML, HTML-atribut, JavaScript, Url med evt. flere).
  • AntiXSS er installeret og benyttes til encoding.
  • INGEN sider tillader HTML-input (uden der er en rigtig god grund til det). Benyt i stedet BB-codes til formatering af indholdet, hvis det er nødvendigt, at brugeren har denne mulighed.
  • Request-validation er slået til for hele sitet.

CSRF (Cross Site Request Forgery)

  • INGEN GET-metoder ændrer på systemets tilstand (dvs. der må kun returneres tilstand ifm. GET-metoder).
  • Alle formularer har AntiForgeryToken og action-metoder der modtager formulardata har ValidateAntiForgeryToken-atributten tilknyttet.

Øvrige

  • CustomErrors i web.config er sat til "On"
  • Alle formularer er beskyttet mod "overposting", dvs. at der sendes flere data med end forventet som modelbinder automatisk fletter ind i modellen på serveren.

Listen er lidt indforstået, men jeg vil se om jeg kan få lavet nogle artikler omkring nogle af emnerne for at kaste lidt mere lys over, hvordan man sikrer sig disse regler overholdes.

Andre resurser

XSS og CSRF beskyttelse i ASP.NET MVC

Jeg sidder pt. og forsøger, at sikre et site mod XSS og CSRF og i den forbindelse er jeg pt. kommet frem til at flg. som minimum bør gøres.

Sikring mod XSS

Her skal man sikre sig at klienten ikke kan injicere skadelig indhold på sitet og dette indbærer, at man konstant (nærmest paranoidt) sørger for at encode sit output. Der er flere forskellige scenarier man skal forholde sig til og nogle af disse er

  1. data der genereres ind i HTML
  2. data der genereres ind i HTML-tag atributter
  3. data der genereres ind i JavaScript

Hver af disse skal håndteres forskelligt og flg. eksempler forsøger at give et bud på, hvordan dette kan gøres. Model indeholder i disse eksempler de tekster der skal genereres ind i siden i et objekt der hedder Translations. Dette er for at signalere, at teksterne kan være variable og derfor ikke kan garanteres værende passende indhold i den givne kontekst.

Ad 1: Dette sker som udgangspunkt, når man bruger @ til at generere output, dvs. at

<strong>@Model.Translations.ImportantText</strong>

som udgangspunkt er HtmlEncoded, fordi @ sørger for dette i MVC 3.

Ad 2: Her skal man gøre sig lidt ekstra umage, men der findes også en helper i frameworket til dette, så data kan indsættes i en atribut således

<a href="/"
    title="@Html.AttributeEncode(Model.Translations.Home)">
        @Model.Translations.Home</a>

Ad 3: Denne løsning ligner lidt atribut-encoding, men formatet er endnu længere

<script>
   var tekst = 
      "@Html.Raw(Ajax.JavaScriptStringEncode(
                    Model.Translations.AlertText))";
   alert(tekst);
</script>

Med dette på plads skulle der være taget hånd om de fleste scenarier mht. encoding. Det skal naturligvis gøres for ALT input der kommer fra en utroværdig kilde - endda også databasen, da den kan gemme på XSS, med mindre man har sørget for at lukke for alt indkommende skadelig indhold på alle kanaler (POSTS, COOKIES, HEADERS osv.).

Håndtering af CSRF

MVC 3 har som standard en atribut til controller actions, som tjekker for om en given forespørgsel indeholder et unikt token og hvis det ikke gør, så fejler forespørgslen helt.

Feltet hedder __RequestVerificationToken og ligger typisk i den formular der sendes med en POST-forespørgsel. Dette felt kan, af MVC frameworket, også nemt genereres og indsættes i formularer, hvor dette måtte være nødvendigt. Dette gøres således

@using(Html.BeginForm())
{
   @Html.AntiForgeryToken()
   <!-- Resten af din fomular her... -->
}

Således er der, i formularen og i en cookie, indsat et unikt token, som serveren kan tjekke på når formularen POSTes tilbage til serveren. Controlleren til formularen kunne så dekoreres således

[ValidateAntiForgeryToken]
public ActionResult Contact(ContactViewModel model)
{
   // do the contactstuff here...
   return View();
} 

Således skulle denne formular være sikret mod at contact-formularen kan sendes fra andre sites end dit eget. Hvis man vil krydre tingene endnu mere, er der også mulighed for at tilføje "salt" til sit token. Dette gøres således begge steder (i formularen og i ValidateAntiForgeryToken-atributten ifm. controller action).

Håndtering af CSRF i et AJAX-scenarium

Hvis man laver meget AJAX på en side, kan det være besværligt at håndtere CSRF, men laver man POST-request flere gange i løbet af sidens levetid, kan dette løses med lidt scripting og én omnipresent formular på siden, der indeholder AntiForgeryToken (som illustreret nedenfor).

@using(Html.BeginForm("", "", FormMethod.Post, 
              new { id = "frmARFTokenForm" }))
{
   @Html.AntiForgeryToken()
} 

I scriptet kan man så aflæse denne og plastre den ind i sin AJAX-POST-forespørgsel. Dette kunne se således ud (her antager jeg at jQuery er inkluderet i siden)

<script>
    $(function() {
       var tokenForm = "#frmARFTokenForm",
           tokenField = "input[name=__RequestVerificationToken]";
  
       $("#sendData").click(function() {
          var contactForm = $("#frmContact");
          var arfToken = $(tokenForm + " " + tokenField).val();

          $.ajax({
             type: "POST",
             url: "/home/contact",
             data: {
                text: $("textarea[name=comment]", contactForm).val(),
                __RequestVerificationToken: arfToken
            },
            success: function(data) {
                // update the page...
            }
         });
      });
   });
</script>

Dette betyder at det er samme token der sendes med alle formularer indtil siden opdateres i browseren, hvorefter frmARFTokenForm opdateres med et nyt token, som så benyttes af de forskellige scripts.

Der er naturligvis basis for en centralisering af denne feature (f.eks. en rutine til at hente ARFToken og en til at (ind)sætte ARFToken-feltet i en given formular), men princippet er i det mindste skitseret...

Unittest af database og strengtest med contains

Jeg sad lige med en test der drillede. Det handlede om at jeg lave et opslag i min database med Linq to Sql, hvor kriteriet var noget i stil med 

q = q.Where(v => 
    v.Description.Contains(txt) 
    || v.Title.Contains(txt)
);

I min test testede jeg så på nogenlunde det samme kriterium, dvs. noget i stil med:

Assert.Istrue(
    actual.TrueForAll(v => 
        v.Description.Contains(txt) 
        || v.Title.Contains(txt)
    ));

Men dette fungerede ikke og min test fejlede!? Hm!

Efter noget roden frem og tilbage, kom jeg til at tænke på, at min database jo nok var sat op til at sammenligne strenge uden hensyntagen til forskellen mellem store og små bogstaver, hvilket jo ikke er tilfældet med String.Contains, der ser forskellen. 

Løsningen i min test blev derfor:

Assert.Istrue(
    actual.TrueForAll(v => 
        v.Description.IndexOf(txt, 
            StringComparison.InvariantCultureIgnoreCase) >= 0
        || v.Title.IndexOf(txt, 
            StringComparison.InvariantCultureIgnoreCase) >= 0
    ));

 Så passerer testen igen... :-)

Repository pattern

Jeg har længe søgt efter en relativ generisk måde hvorpå jeg kan strukturere mit repositoryinterface og jeg er efterhånden kommet frem til et repository interface, som dækker de fleste af mine behov for CRUD og listeudlæsninger.

void Load(FO, id); 
void Save(FO); 
void Delete(id); 
IEnumerable<FO> GetList(count, page, out total, criteria);

Load modtager et instantieret forretningsobjekt (her betegnet FO, men skal have den konkrete type eller et interface) og fylder objektet med data fra repository med det angivne id. Hvis data ikke findes rejses en kørselsfejl, for at undgå at objektet skal implementere en sporing af, om data er indlæst eller ej. Alternativt kan Load returnere en boolsk falsk.

Save tager data i FO og oprette eller opdatere data i repository. Id i FO opdateres til at modsvare data i repository, hvis der er tale om en oprettelse i repository.

Delete sletter data for FO i repository og nulstiller FO's id, således dette kan gemmes igen og dermed foranledige en oprettelse, hvis dette skulle findes nødvendigt.

GetList modtager nogle parametre som hjælper med paginering af udtrækket. Count er det antal elementer der skal udtrækkes og page er den side i det total udtræk (jf. de øvrige parametre i criteria-objektet), hvorfra elementerne skal udtrækkes. Total er det antal elementer der total set er i udtrækket. Criteria indeholder et (variabelt) antal parametre, som er relevante for det aktuelle FO's kontekst. Dette kunne f.eks. være søgetekst eller hvorvidt de udtrukne FO skal have en bestemt status. Det er helt op til kravene til FO og systemet.

I praksis

Et eksempel på hvordan dette kan tage sig ud i praksis kunne se ud som vist i det følgende.

Præmisserne her er, at data gemmes i en SQL Server og ORM er LinqToSql. Der tages udgangspunkt i et personregister, hvor hver person har registreret nogle oplysninger omkring sig, der giver lidt muligheder for at udforske metoden omkring søgning og paginering. Forretningsobjektet spejler i dette eksempel tabellen person databasen, så definitionen af databasen er undladt her.

Først defineres FO:

public class Person
{
   public int Id { get; set; }
   public string Navn { get; set; }
   public string Adresse { get; set; }
   public string Postnr { get; set; }
   public string Bynavn { get; set; }
   public string Email { get; set; }
   public string Telefonnr { get; set; }
}

Kriterieobjektet kunne se således ud:

public class PersonSearchCriteria
{
   public string searchText { get; set; }
}

Dernæst defineres repositoryobjektet:

public class PersonRepository
{
   public void Load(Person person, int id)
   {
      using(var db = new personDataContext())
      {
         var dbPerson = db.person.SingleOrDefault(p => p.Id == id);
         if(dbPerson != null)
            throw new Exception("Person ikke fundet");

         person.Id = dbPerson.Id;
         person.Navn = dbPerson.Navn;
         person.Adresse = dbPerson.Adresse;
         person.Postnr = dbPerson.Postnr;
         person.Bynavn = dbPerson.Bynavn;
         person.Email = dbPerson.Email;
         person.Telefonnr = dbPerson.Telefonnr;
      }
   }

   public void Save(Person person)
   {
      using(var db = new personDataContext())
      {
         var dbPerson = 
                 db.person.SingleeOrDefault(p => p.Id == person.Id);
         if(dbPerson == null)
         {
            dbPerson = new person();
            db.person.InsertOnSubmit(dbPerson);
         }
         dbPerson.Navn = person.Navn;
         dbPerson.Adresse = person.Adresse;
         dbPerson.Postnr = person.Postnr;
         dbPerson.Bynavn = person.Bynavn;
         dbPerson.Email = person.Email;
         dbPerson.Telefonnr = person.Telefonnr;

         db.SubmitChanges();

         person.Id = dbPerson.Id;
      }
   }

   public void Delete(int it)
   {
      using(var db = new personDataContext())
      {
         var dbPerson = db.person.SingleOrDefault(p => p.Id == id);
         if(dbPerson != null)
         {
            db.person.DeleteOnSubmit(dbPerson);
            db.SubmitChanges();
         }
      }
   }

   public IEnumerable<Person> GetList(
         int count, 
         int page, 
         out int total, 
         PersonSearchCriteria criteria
      )
   {
      using(var db = new personDataContext())
      {
         var list = db.person.AsQueryable();

         if(!String.IsNullOrWhiteSpace(criteria.searchText))
         {
            var txt = criteria.searchText;
            list = db.person.Where(p => 
                     p.Navn.Contains(txt)
                     || p.Adresse.Contains(txt)
                     || p.Postnr.Contains(txt)
                     || p.Bynavn.contains(txt)
                     || p.Email.Contains(txt)
                     || p.Telefonnr.Contains(txt));
         }

         total = list.Count();

         if(count > 0)
         {
            if(page > 1)
               list = list.Skip((page - 1) * count);

            list = list.Take(count);
         }

         return 
            from p in list
            select new Person {
               Id = dbPerson.Id,
               Navn = dbPerson.Navn,
               Adresse = dbPerson.Adresse,
               Postnr = dbPerson.Postnr,
               Bynavn = dbPerson.Bynavn,
               Email = dbPerson.Email,
               Telefonnr = dbPerson.Telefonnr
            };
      }
   }
}

Med dette kan man udtrække en liste af alle personer i databasen eller en liste af personer som møder visse kriterier, her et ord som indgår i navn, adresse, postnr, by, email eller telefonnr. Kriterierne kan udvides efter behov og selve kaldet til databasen bliver aldrig mere komplekst end der er behov for, fordi kriterierne undlades, hvis der ikke er specificeret nogen søgetekst. På samme måde undlades paginering, hvis count, dvs. det ønskede antal personer, er mindre end 1 (reglen er implicit eller pr. konvention om man vil).

Brugen af repository kunne se således ud og ligge som en static metode på Personobjektet eller i en factoryklasse til Person.

Factoryklassen kunne se såleds ud:

public class PersonFactory
{
   private readonly PersonRepository _repository;

   public PersonFactory(PersonRepository repository)
   {
      _repository = repository;
   }

   public IEnumerable<Person> GetAll(
         int count, 
         int page, 
         out int total
      )
   {
      return _repository.GetList(count, page, total, null);
   }

   public IEnumerable<Person> Search(
         int count, 
         int page, 
         out int total, 
         string searchText
      )
   {
      var criteria = new PersonSearchCriteria { 
            searchText = searchText 
         };
      return _repository.GetList(count, page, total, criteria);
   }
}

Som det kan ses, implementerer factoryklassen et mere specialiseret interface der ligger tættere på forretningskravene og som er mere ligetil at benytte. Dette sker uden at repository-interfacet bliver forplumret af flere metoder.

Hvis man ønsker at implementere flere kriterier med tiden, skal disse blot lægges i PersonSearchCriteria-objektet og derefter skal GetList-metoden ændres så de nye kriterier håndteres. Dermed ændres der ikke på repositoryinterfacet og man sparer derfor, at skulle rette i evt. andre klienter som er afhængige af respositoryklassen.

Se også

En artikel om et generisk repository, som kan gøre hele arbejdet med repositories noget enklere.

Unittest af Data Access Layer

Jeg har på det seneste kastet mig ud i at teste Data Access Layer i et projekt. Projektet er baseret på SQL Server med Linq2Sql som ORM.

Jeg har længe udskudt denne aktivitet, da det virkede uoverkommeligt at skulle håndtere databasetilstand før og efter tests og har i det hele taget været i tvivl om hvordan jeg skulle gribe det an, med Linq2Sql som ORM. Nu har jeg taget tyren ved hornene og kastet mig ud i det. I den forbindelse har jeg gjort nogle foreløbige erfaringer som jeg lige vil lave et lille notat om...

Isolering af databasen

Som udgangspunkt vil jeg gerne have adskilt min testdatabase fra den database jeg benytter til at lave lokale udforskende tests på (dvs. de tests jeg laver når jeg kører det site jeg arbejder med). Dette er dels for ikke at fucke de data jeg sidder og laver mens jeg tester udforskende, dels for at kunne have "ren røv at trutte i", til mine automatiske tests.

Derfor har jeg replikeret skemaet fra min udviklingsdatabase og oprettet en dedikeret autotest-database, som jeg så peger på fra min testprojekts app.config.

Før hver test kører jeg en wipe af databasen. Dette gøres fra en metode dekoreret med AssemblyInititalize, hvor jeg udfører truncate/delete på alle relevante tabeller.

Derefter sætte jeg nogle basale data ind i databasen, da der er nogle data som skal være til stede for at systemet kan komme igang (f.eks. en bruger der kan refereres til fra mange andre tabeller).

Opbygning af tests

Når man så skal igang med at skrive tests, er det vigtigt at tænke på samtidigheden i udførslen af tests, hvilket bla. betyder, at man ikke altid kan være sikker på, hvilke data der ligger i databasen på et givet tidspunkt. Derfor kan tests ikke blindt gå ud fra at der er et specifikt antal elementer i databasen, da flere tests, på et vilkårligt tidspunkt i testforløbet, kan oprette eller slette data.

Tests bør såvidt muligt teste for tilstande, som er statiske fra gang til gang, dvs. hvis en funktion der returnerer en liste af elementer ud fra et eller andet kriterium, så bør man teste om dette kriterium er opfyldt i alle de returnerede elementer i stedet for at teste om et bestemt antal er returneret (med mindre det rent faktisk er et kriterium for testen).

Et eksempel på hvordan man kan gøre dette, kunne se således ud:

var target = new PersonSQLRepository(GetLocator());

List<IPerson> actual = target.GetPersonList(true);

Assert.IsTrue(actual.TrueForAll(p => p.Activated));

GetLocator er blot en funktion der returnerer en Service Locator, der benyttes til at oprette IPerson-objekter inde i PersonSQLRepository-klassen. Parameteren til GetPersonList angiver, om personen skal være aktiveret.

Hvis det f.eks. havde være en test af paginering af det returnerede data, ville det være OK at teste om antallet af elementer oversteg det ønskede antal og evt. om det totale antal gav anledning til at testen burde fejle.

Et eksempel på dette kunne se således ud:

var total = 0, 
    target = new PersonSQLRepository(GetLocator());

List<IPerson> actual = target.GetPersonList(10, 1, out total, true);

Assert.IsTrue(total>=10 ? actual.Count==10 : actual.Count==total);
Assert.IsTrue(actual.TrueForAll(p => p.Activated));

GetPersonList's parametre betyder: antal elementer der ønskes returneret, sidenr, antal relevante elementer ialt i databasen, samt kriteriet fra tidligere med kravet om aktiverede personer. Total indeholder, efter kaldet, det faktiske antal personer der møder kriteriet.

Som det ses tages der stilling til forskellige scenarier f.s.v.a. antal returnerede personer, hvilket her er OK, da kravene til værdisættet er statisk.

Opsummering

Ifm. med test af DAL bør databasen isoleres og man skal lave sine tests defensivt, dvs. ikke gøre antagelser om det datasæt man arbejder med, men blot forsøge at forholde sig tilstanden af de data der returneres holdt op mod de forventninger man har til data.

Oversættelse vha Custom Data Annotations

Arbejder man med internationale web applikationer, er man nok stødt på problemstillingen om, hvordan man oversætter sine sider. Der findes mange metoder, men når nu man er lidt doven og gerne vil undgå for meget arbejde, gælder det jo om at finde den metode som involverer mindst arbejde. Desuden kan man jo lige så godt bruge de metoder som frameworket tilbyder.

I mit arbejde med ASP.NET MVC 3 er jeg stødt på denne metode, der muligvis kunne spare mig for noget kode og kompleksitet andre steder i mit system. Det drejer sig om nedarvning af DataAnnotationsModelMetaDataProvider-klassen, hvor metoden CreateMetadata overstyres, således DisplayName-egenskaben oversættes. Det kunne se nogenlunde således ud: 

public class CustomModelMetadataProvider 
                : DataAnnotationsModelMetadataProvider
{
    protected override ModelMetadata CreateMetadata(
        IEnumerable<Attribute> attributes, 
        Type containerType, 
        Func<object> modelAccessor, 
        Type modelType, 
        string propertyName)
    {
        var metaData =  base.CreateMetadata(
            attributes, 
            containerType, 
            modelAccessor, 
            modelType, 
            propertyName);

        metaData.DisplayName = 
            metaData.GetDisplayName().Translate();

        return metaData;
    }
}

Den nye provider skal aktiveres i Global.asax, således den bliver kaldt når dataannotations skal behandles. Dette gøres i Application_Start således:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    ModelMetadataProviders.Current = 
        new CustomModelMetadataProvider();

    RegisterStorageConfiguration();
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    // ... og hvad vi ellers har...
}

Med dette på plads, kan (view)model udstyres med en nøgle til oversættelsen. Det kunne se således ud:

public class Person
{
    [Display(Name="First name")]
    public string FirstName { get; set; }

    [Display(Name="Last name")]
    public string LastName { get; set; }

    public string Address { get; set; }
}

Bemærk at for egenskaber uden Display-annotation vil det være egenskabens navn er benyttes som nøgle til oversættelse. Nu vil Person-objektets data kunne vises i et view således med oversatte ledetekster:

@model Person
@{
    Layout = null;
}

@Html.DisplayForModel()

Selve oversættelsen sker i Translate-funktionen, der i mit tilfælde er en Extension på string-objektet og som slår op i et datalager, hvor hver tekst er oversat til det aktuelle sprog. Hvilket sprog der er tale om, hentes (af Translate extension funktionen) i den aktuelle tråds UI Culture.