Fremmednøgletildeling i LINQ to SQL

Jeg har bakset lidt med en fejl, der opstod i forbindelse med opdatering af en LINQ to SQL-entity. Helt konkret drejer det sig om den lidet informative fejl: "Operation is not valid due to the current state of the object", som opstår i forbindelse med at jeg tildeler en værdi til en fremmednøgle i denne entity.

Setup'et

Jeg opererer med optimistisk samtidighed og et afkoblet datasæt, dvs. jeg læser objektet, lukker forbindelsen og skaber en ny forbindelse, når jeg skal opdatere data (et typisk scenarium for websites der involverer en bruger mellem læsning og opdatering). I forbindelse med selve opdateringer indlæser jeg først objektet (og beholder forbindelsen til databasen), opdaterer entity'objektet og opdaterer databasen. Jeg har altså gang i noget med at kopiere data fra entity over i et forretningsobjekt og tilbage igen ifm. opdatering.

Idet min entity læses sætter jeg nogle LoadOptions på forbindelsen, så relevante relaterede rækker fra andre tabeller også indlæses. Dette gøres af performancemæssige hensyn, da en reference til disse relaterede entities ville forårsage en "lazy loading" af data og dermed endnu et opslag i databasen.

Løsningen

Denne "eager loading" af relaterede data giver problemer, hvis jeg forsøger at opdatere fremmednøglen og ikke samtidig sætter det relaterede objekt til den nye entity (som nøglen også refererer til).

Jeg skal altså IKKE preloade relaterede tabeller, hvis jeg blot ønsker at benytte fremmednøglen til opdateringshandlinger.

Alternativt kunne jeg måske klare mig ved at benytte Attach i stedet for at læse entity inden opdatering. I dette scenarium ville der nok ikke ske en "eager load" af relaterede tabeller (jf. LoadOptions). Det har jeg dog ikke eksperimenteret med endnu...

Se også

http://www.faridesign.net/2010/11/linq-to-sql-operation-is-not-valid-due-to-the-current-state-of-the-object/

DotNetOpenAuth og MVC 5

Jeg har et projekt som kører .NET 4.5 og MVC 5, hvor der også er noget OAuth involveret. I den forbindelse forsøger jeg at lave noget Reflection, men får en fejl som omhandler sikkerhed. Det viser sig at DotNetOpenAuth ikke er compatibel med MVC 5, fordi der er sket en ændring i måden sikkerheden håndteres i MVC 5 ifht. MVC 4. Desværre vedligeholdes DotNetOpenAuth ikke aktivt i øjeblikket, men der er dog lavet en eller anden løsning til problemet... jeg har bare ikke været i stand til at få den til at fungere.

Workaround

I stedet for at gennemløbe alle typer i alle assemblies, nøjes jeg med at gennemløbe dem i den assembly jeg har brug for (hvilket så gør, at jeg undgår projektet med DotNetOpenAuth-dll'erne) og problemet er dermed gemt af vejen.

Der findes som sagt en opdatering til DotNetOpenAuth som er rettet mod MVC 5 og den kan hentes via NuGet:

Install-Package DotNetOpenAuth.Mvc5

 

Umbraco pre v4 database oprydning

Jeg havde for nylig et site der kørte på Umbraco og som kørte enormt langsomt og fungerede egentlig ikke rigtigt. Jeg undrede mig en del over det, for indholdsmæssigt var det ikke et tungt site (kun få sider - mindre end 100), men et site der havde været i drift i en længere periode. Jeg besluttede mig for at finde ud af hvorfor det ikke fungerede. Her er hvad jeg fandt frem til...

Symptomerne

Sitet kørte som nævnt ikke rigtigt. Helt konkret var det enormt langsomt til at indlæse træstrukturen og det var et held, hvis man fik lov til at oprette nye dokumenter.

Det er klart, at man ikke kan forvente hvad som helst af en 10+ år gammel undskyldning for en server, men jeg havde da også andre Umbraco-sites på denne server, som slet ikke havde problemer med at køre, så jeg mente jo nok dette ikke burde være et problem. Også det faktum, at der var tale om et Umbraco v3.0.6 kunne vække en vis bekymring, men igen, så var der også andre sites i denne version, på samme server, som kørte tilfredsstillende.

Opdagelsen

Efter af have kigget lidt rundt i Umbraco-admin, uden dog at kunne finde noget, der virkede himmelråbende tåbeligt konfigureret, bedrog jeg mig ud i stifinderen, for at se om der kunne være noget der. Første stop web.config, men også her så tingene tilforladelige ud, så heller ikke der synes jeg, der var noget at hente.

Efter lidt stikken og justeren, fandt jeg frem til databasefilerne (data og log) for sitet og til min store overraskelse opdagede jeg, at data-filen fyldte 2.3+ Gb! Det var vist ikke nødvendigt for sådan et lille site, så noget måtte være galt, men hvad!?

Undersøgelsen

Gad vide hvordan sådan en Umbraco database er bygget op? En hel masse tabeller præfikset med cms og umbraco og lidt svær at gennemskue, hvis man ikke har arbejdet indgående med sådan en før (hvilket jeg ikke havde), så Google måtte kunne hjælpe...(?)

Først og fremmest måtte jeg have et overblik over hvormeget data der var i hvilke tabeller. Til dette formål fandt jeg flg. SQL-script, som, om en given database, kunne fortælle mig, hvormange rækker der er i hver tabel, hvormeget tabellens indhold fylder i Kb m.m.

SELECT 
    t.NAME AS TableName,
    s.Name AS SchemaName,
    p.rows AS RowCounts,
    SUM(a.total_pages) * 8 AS TotalSpaceKB, 
    SUM(a.used_pages) * 8 AS UsedSpaceKB, 
    (SUM(a.total_pages) - SUM(a.used_pages)) * 8 AS UnusedSpaceKB
FROM 
    sys.tables t
INNER JOIN      
    sys.indexes i ON t.OBJECT_ID = i.object_id
INNER JOIN 
    sys.partitions p ON i.object_id = p.OBJECT_ID 
                    AND i.index_id = p.index_id
INNER JOIN 
    sys.allocation_units a ON p.partition_id = a.container_id
LEFT OUTER JOIN 
    sys.schemas s ON t.schema_id = s.schema_id
WHERE 
    t.NAME NOT LIKE 'dt%' 
    AND t.is_ms_shipped = 0
    AND i.OBJECT_ID > 255 
GROUP BY 
    t.Name, s.Name, p.Rows
ORDER BY 
    t.Name

Efter at have kørt dette script i SSMS, fik jeg en fornemmelse af, at problemet havde med versionering af dokumenterne at gøre. De tabeller som indeholdte markant mere data end forventet, var cmsContentVersion, cmsDocument, cmsPropertyData og umbracoLog. Uden at ville (og kunne) gå i yderligere detaljer om, hvad disse tabeller konkret bruges til, indikerer det i det mindste, at det har noget med versionering at gøre. Spørgsmålet er bare, hvorfor? Der er ikke specielt meget aktivitet i relation til vedligeholdelse af indhold på sitet, så hvorfor er der oprettet så mange rækker i disse tabeller (der var over 500.000 i cmsDocument og cmsContentVersion og over 3.500.000 i cmsPropertyData, ligesom der også var omkring 1.200.000 i umbracoLog...!

Et kig i umbracoLog afslører en periodisk publisering af to specifikke dokumenter, som resulterer i 4 nye rækker i umbracoLog, samt nye 2 eller flere rækker i hver af de andre tabeller, hver gang. Hver gang vil sige, hvert minut 24/7/365 - det bliver til en del rækker med tiden... :-)

Det var naturligvis ikke en optimal situation, så der måtte da være en løsning på dette. Hvorfor blev disse to dokumenter (indholdssider på linje med en masse andre på sitet) publiseret med 1 minuts intervaller og mere vigtigt, hvordan fik jeg det så stoppet!?

"Løsningen"

Derfor måtte jeg finde ud af, hvordan jeg kunne løse dette. Google er min ven(!), men selv venner har sine begrænsninger, så det lykkedes den ikke at fremskaffe en løsning. Den eneste tilnærmelsesvis overkommelige løsning var, at opgradere til en nyere version, hvor dette ikke længere var et problem. Det lader nemlig til at være et bug i nogle versioner (herunder v3.0.6, som dette site jo kører), som bevirker, at der publiseres på denne måde.

Jeg skal erkende, at jeg heller ikke har søgt efter en løsning meget mere end en times tid, da sitet skulle pilles ned indenfor en overskuelig tidshorisont, så det kan være der rent faktisk kan køres et workaround, som løser dette, men jeg er som sagt ikke faldet over det.

Det jeg så gør i stedet, er symptombehandling (when all else fails...). Det indbefatter en periodisk udførsel af et SQL script, som sletter alle versioner op til en bestemt dato (i nedenfor viste script, er det til dagen før den dato hvorpå scriptet udføres) og som ikke er den aktive version. Dette script ser således ud.

DECLARE @createdDate Datetime
SET @createdDate = DateAdd(day, -1, getdate())

DELETE FROM cmsPropertyData WHERE
    versionId NOT IN (
              SELECT versionId 
              FROM cmsDocument 
              WHERE updateDate > @createdDate 
                 OR published = 1 
                 OR newest = 1) AND
    contentNodeId IN (SELECT DISTINCT nodeID FROM cmsDocument)

DELETE FROM cmsContentVersion WHERE
    versionId NOT IN (
              SELECT versionId 
              FROM cmsDocument 
              WHERE updateDate > @createdDate 
                 OR published = 1 
                 OR newest = 1) AND
    ContentId  IN (SELECT DISTINCT nodeID FROM cmsDocument)

DELETE FROM cmsDocument WHERE
    versionId NOT IN (
              SELECT versionId 
              FROM cmsDocument 
              WHERE updateDate > @createdDate 
                 OR published = 1 
                 OR newest = 1) AND
    nodeId IN (SELECT DISTINCT nodeID FROM cmsDocument)

Dette script, når det bliver gemt som en fil og placeret på serveren, kan så, med jævne mellemrum, aktiveres, som en baggrundsopgave på serveren, hvor scriptet kaldes med sqlcmd.

sqlcmd -S myServer\instanceName -i C:\sqlscripts\umbracoCleanup.sql

Konklusion

Nu har jeg i det mindste fået lidt styr på databasevæksten på mit site, selvom jeg ikke synes dette er den optimale løsning (langt fra - det bedste ville jo være, at publiseringen ikke gik i selvsving!). Nogle gange er vi bare nød til, at nøjes med mindre, så vi kan komme videre med det der er rigtig fedt! :-)

 

Upload af store filer i asp.net

Det er ofte et krav, at der skal kunne uploade filer til en webapplikatione. Dette er heldigvis ret let, når der er tale om en ASP.NET webapplikation. Det giver dog til tider lidt problemer, når der skal kunne uploades større filer (over 4 Mb), men ikke større end at det også kan løses. Jeg vil her kort beskrive en løsning.

Ændr indstillingerne

Det er faktisk ikke mere kompliceret end, at ændre indstillingerne i web.config. Helt konkret er det elementet httpRuntime (som ligger under system.web-elementet), der skal ændres således:

<httpRuntime 
    maxRequestLength="8096" 
    requestTimeout="120" 
    requestLengthDiskThreshold="8096" />

Hvis du kører på en IIS 7+ skal du muligvis også tilføje  security-elementet (som ligger under sektion system.webServer-elementet) således:

<security>
  <requestFiltering>
    <requestLimits maxAllowedContentLength="8096000" />
  </requestFiltering>
</security>

Nu skulle du være i stand til at uploade 8 Mb i stedet for 4 Mb. Husk at requestTimeout skal være længere end den længste forventede tid der går fra en klient starter upload til det er færdigt, ellers fejler uploadoperationen med timeout i stedet.

Advarsel

Vær også opmærksom på, at højre grænser for mængden af data der kan sendes til en side i din webapplikation, kan have alvorlige konsekvenser for din sårbarhed overfor DOS-angreb, da det bla. vil optage en tråd i puljen indtil data er modtaget og behandlet. Mange af denne slags kald vil derfor risikere vil udsulte puljen af tråde til at behandle requests, hvilket medfører, at din applikation er nede. Derfor skal det overvejes nøje, hvis denne grænse sættes op til store værdier. Jeg har ikke fået undersøgt, om dette kan afbødes med async/await, men det var måske en mulighed. Dog vil du måske stadig ende med en ekstraregning for trafik efter et DOS-angreb af denne slags...

Se også

http://forums.asp.net/t/1810854.aspx er en tråd med reflektioner omkring dette emne og inspirationskilden til dette indlæg.

Ikke standard 404 handler i ASP.NET MVC 3

Jeg vil, i tilfælde med indhold som ikke eksisterer, gerne returnere en side med statuskoden 404 (af hensyn til SEO), men samtidig gerne have, at siden ikke er en standard 404-side. Dette sker som udgangspunkt ikke i ASP.NET MVC3, da MVC 3 kobler en standard handler på fejlsider. Dette er også helt fint, men i disse særlige tilfælde, hvor jeg gerne selv vil styre 404-siden, er det træls.

Efter lidt søgen rundt på nettet (primært StackOverflow), fandt jeg, i en kommentar til et indlæg, en henvisning til denne egenskab på Response-objektet:

Response.TrySkipIisCustomErrors = true;

Den sørger åbenbart for, at den standard handler som er opsat, ikke aktiveres, når jeg sætte Response.StatusCode = 404.

Dermed kan jeg med følgende kode i min Controller's Action, få vist en ikke standard 404-side (som, i mit tilfælde, ligger i et view med navnet "NotFound"):

public ActionResult View(int id)
{
   Response.Status = "404 - Not Found";
   Response.StatusCode = 404;
   return View("NotFound");
}

Jeg kunne måske endda pakke denne kode ind, så jeg kan kalde den fra forskellige controllers, som så kunne have deres egne specialiserede views til NotFound-håndtering...

Bundling og minification i ASP.NET MVC 3

Har man arbejdet med ASP.NET MVC 4, ved man der findes en bundling og minification mulighed indbygget i frameworket. Dette er umiddelbart nyt ifht. MVC 4 og fandtes ikke som udgangspunkt i MVC 3.

Man kan dog benytte sig af disse features i et MVC 3-projekt, hvis man benytter .NET 4, man skal bare lige selv sætte de grundlæggende ting op i sit projekt (registrering af bundles, inkludering af System.Web.Optimization og kald af bundles i sit layout).

Du kan finde en nærmere forklaring til dette i artiklen om "Using MVC 4 bundling and minification in an MVC 3 project".

Se også

Denne artikel på MSDN er endnu mere uddybende omkring emnet.

"Javascript minification in MVC 3" kommer også ind på, hvordan man kan overstyre bundling- og minificationprocessen, så man kan smide sine egne regler ind i mix'et.

Hvis du arbejder med webforms kan du måske få lidt ud af kigge på, hvordan man kan benytte scriptmanager til at kombinere scriptfiler.

Azure Emulator og SSL

Jeg arbejder på en webapplikation der skal køre i Azure og jeg har dermed også behov for at kunne teste lokalt.

Mit setup er:

  • Visual Studio 2012
  • ASP.NET MVC 3
  • .NET 4.0
  • Windows 8 Pro x64
  • fuld IIS 8

Denne webapp skal køre en del af sitet over SSL, så derfor har jeg fået sat mit udviklingsmiljø op til at benytte SSL, hvilket umiddelbart også fungerer.

Jeg har dog oplevet lidt problemer med, at uploade filer over SSL. Dette kan nok skyldes mange ting, men umiddelbart udmynter det sig i, at jeg får en 404 tilbage, når jeg POST'er filen til en, 100% sikkert, eksisterende action på min controller. Først troede jeg det havde noget med mine routing at gøre, men GET-request til samme action fungerer jo, så jeg har udelukket denne mulighed.

Jeg har endnu ikke fundet en løsning på dette problem, men jeg har fundet et workaround.

Det jeg gør er, i stedet for at køre over den localhost IP, som IDE'en starter min browser på (https://127.0.0.1/), så kører jeg over den localhost IP som IIS manageren peger på, nemlig https://127.255.0.0:444/

Porten kan dog variere, men hvis man lurer i Output-vinduet af VS, når Emulatoren startes (ifm. build and run), skal man bruge det private portnummer, hvilket kan ses i den linje hvor der står

Windows Azure Tools: Warning: Remapping private port 443 to 446 in role...

Når jeg gør det, lader det til at min action bliver aktiveret korrekt og filen bliver uploadet som den skal.

Integrer et site med Open Graph

Jeg fik lige et spørgsmål fra en ven, som omhandlede LinkedIn's valg af billede fra en side når der henvises til den fra LinkedIn. Det fik mig så til at lure lidt på Open Graph definitionen (via et link fra en anden ven :-)).

Dette er sådan facebook gør det og givetvis også LinkedIn m.fl.

Kort sagt kan man smide et antal meta-elementer ind i head-sektionen af siden, som angiver hvad der skal tilbydes overfor det sociale medie. Dette kunne f.eks. være 

<meta property="og:image" content="http://example.com/rock.jpg" />
<meta property="og:title" content="Titel på siden" />
<meta property="og:description" content="Beskrivende tekst" />

for at angive hvilket billede man ønsker skal kunne benyttes, samt hvilken titel og beskrivelse der skal følge linket.

Der findes et hav af muligheder for at styre, hvad der skal bruges ifht. de sociale medier og man kan få smag for sagerne ved at kigge på denne siden med The Open Graph Protocol.

Man kan evt. teste på facebook med deres værktøjer (det kræver dog, at man logger på udviklersitet, hvilket ikke burde give nogen problemer for nogen)... 

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.