Använd POCO-stödet i Entity Framework 4

I första versionen av Entity Framework som kom i .NET 3.5 SP 1 fanns till största delen grundläggande funktionalitet för att kunna koppla sig mot en databas snabbt och enkelt och utföra operationer. Dock saknades väldigt mycket, och på grund av en mängd beroenden bland de klasser som genererades så blev det svårt att enkelt separera på logiken. I .NET 4.0 introducerades en mängd funktioner för att lösa detta, och det är bland annat det jag kommer att använda mig utav här.

Projektet jag kommer att ta upp kommer att bli ett enkelt ”bibliotek” där man kan lista författare och böcker de har skrivit.

Förutom en vanlig installation av Visual Studio 2010 och SQL Server (2005 eller senare) så kommer dessa att användas:

Skapa upp projekten

Det första vi behöver göra nu är att skapa upp de projekt vi kommer att använda oss utav när vi testar all kod. För enkelhetens skulle skapar vi upp alla projekt på en gång (även de vi inte kommer att använda till en början).

  • De projekt vi kommer att ha är:
  • Data\Library.EntityRepository (Class Library)
  • Data\Library.TestRepository (Class Library)
  • UI\Library.ConsoleApplication (Console Application)
  • UI\Library.Mvc (ASP.NET MVC 2 Application)
  • Library.Interfaces (Class Library)
  • Library.Model (Class Library)

Jag har även med en Solution Folder med dependencies samt ett Modeling Project för att kunna generera ett Layer Diagram som enkelt kan visa dependencies mellan projekt. Dessa är dock ej nödvändiga för projektet.

Solution Explorer ser nu ut så här:

1 - Solution Explorer

Hur de olika projektens beroenden ser ut kan ni se här:

3 - Layer Diagram

Domain (Library.Model) och Interfaces (Library.Interfaces) har inga beroenden. De två datalagren har dock referensen till dessa två. UI-lagren har referenser till Domain, Interfaces och de olika datalagren.

Vi behöver även en databas att jobba mot. Då Entity Framework har funktionalitet för att skapa tabeller baserat på en modell så kommer vi att starta med en helt tom databas. Det som krävs är SQL Server 2005 eller senare (SQL Server 2000 har ingen support längre, och stödet är borttaget i Visual Studio 2010 för det). Jag kommer att använda SQL Server 2008 R2 Express, vilket fungerar utmärkt att använda. Man behöver alltså inte använda fulla versionen, utan kan utan problem köra med Express-versionen.

2 - Management Studio

Den skapade databasen, utan tabeller då de skapas av Entity Framework senare.

Skapa modellen

När vi nu har ett projekt och en databas så ska vi börja med att skapa Entity Framework-modellen, från vilken vi även skall skapa tabellerna i databasen. För att kunna ta nytta av allt jag tar upp nu så är det viktigt att ni har installerat stödet för det, vilket jag länkar till i början av artikeln.

I EntityRepository-projektet kommer vi att ha själva modellen, dess repository och andra filer som kommer att användas utav Entity Framework.

Börja med att välja Add -> New Item -> ADO.NET Entity Data Model. Vi ger filen namnet LibraryEntities.edmx och skapar den. Vi väljer sedan att skapa en Empty Model då vi vill skapa databasen utifrån modellen och inte tvärtom.

4 - Empty Model

De två entiteter vi kommer att använda nu är Author och Book. En Author kommer att kunna ha flera Books och en Book kommer att ha en Author. För att skapa en entitet så högerklicka på den tomma vita ytan och välj Add -> Entity.

5 - Add Entity

Den första entiteten heter Author. Se till att sätta Entity Set till Authors och inte AuthorSet som det står som standard. Vi ser även till att ha ett ID med namnet AuthorId. Klicka på OK och gör detsamma för Book. Det vi har nu är två entiteter utan egenskaper.

Nästa steg blir att skapa de olika egenskaperna. Högerklicka i Author och välj Add -> Scalar Property. Sätt namnet till Name. Gör sedan samma sak för Book där vi istället har Title.

Nu har vi två entiteter utan någon relation till varandra. Högerklicka nu i den vita ytan och välj Add -> Association. Det vi får upp nu är:

6 - Association

Ändra Book till Books för den Navigation Property som tillhör Author då vi ska ha flera böcker för en författare. Se även till att resten ser ut som på bilden och klicka på OK.

Nu har vi två entiteter med en relation mellan varandra, vilket är precis vad vi vill ha.

7 - Entities with association

Nästa steg blir att skapa tabellerna i databasen. Högerklicka nu återgen i den vita ytan och välj nu Generate Database from Model.

När Generate Database Wizard visas så skapa en connection string till databasen som kommer att användas och klicka på Next. Det som händer nu är att Visual Studio läser av edmx-filen (den som används av Entity Framework och innehåller entiteterna vi skapade), och sedan genererar en SQL-sats som kommer att användas för att generera databasen. SQL-satsen sparas sedan ned, vilket gör att vi enkelt kan skapa upp databasen på nytt, vilket är bra om vi vill skapa en likadan i skarpa miljön.

8 - DDL

I kommentar längst upp kan vi se att SQL-satsen fungerar för SQL Server 2005, 2008 och Azure. Det betyder att om vi nu vill använda Azure för vår applikation så kan vi använda samma modell även där.

När vi klickar på Finish visas SQL:en som just genererades , samt att sql-filen skapas och läggs i projektet. Tabellerna har dock ej skapats upp i databasen än, utan det vi får göra nu är att högerklicka i den öppnade filen med all SQL och välja Execute SQL. Kikar vi i databasen nu så kan vi se att tabellerna samt relationerna har skapats upp åt oss.

9 - Generated tables

Vi skulle redan nu kunna använda modellen för att jobba mot databasen, men då kan vi inte ta del av den nya POCO-funktionaliteten. Om vi kikar i Solution Explorer så kan vi se att vi inte bara har en edmx-fil, utan även en tillhörande Designer.cs-fil.

10 - edmxcs

Det cs-filen innehåller är all logik för modellen. Det vi vill göra istället är att skapa en egen ObjectContext-fil som kommer att användas. För att få bort cs-filen så väljer vi att högerklicka på edmx-filen och välja Properties. Här tar vi bort värdet för Custom Tool.

11 - Custom tool

När vi har gjort det så försvinner cs-filen och vi har bara modellen kvar.

Det vi ska göra nu är att skapa upp POCO-klasserna, så högerklicka på projektet, välj Add -> New Item och där ADO.NET POCO Entity Generator. Vi väljer LibraryPoco.tt som namn på filen och skapar upp den. Nu kan det hända att en varning dyker upp, välj då OK för att gå vidare.

Nu har två filer skapats upp, LibraryPoco.tt och LibraryPoco.Context.tt. Ta nu bort Context-filen och flytta LibraryPoco.tt till Library.Model-projektet. Öppnar vi nu upp LibraryPoco.tt så kan vi se att den har ett väldigt annorlunda utseende, och det är då det är en T4-template. T4-templates används för att generera kod, vilket vi kan se om vi expanderar den. Där kan vi se att det finns en cs-fil, vilken har genererats. Vi kommer även att få de genererade modellerna från Entity Framework-modellen här.

Nästa steg blir att sätta sökvägen till edmx-filen i tt-filen.

På rad 24 kan vi se detta:

string inputFile = @"$edmxInputFile$";

Byt ut $edmxInputFile$ mot den relativa sökvägen till din edmx-fil. I mitt fall blir det:

string inputFile = @"..\Library.Data\LibraryEntities.edmx";

Kompilera sedan om projektet. Det som har hänt nu är att T4-templaten har använts för att generera två nya cs-filer, en för varje entitet i modellen:

12 - Generated model

Öppnar vi upp dessa så kan vi se att vi har fått rena POCO-klasser med de propertys vi har, samt relationen mellan dessa. Ingen av dessa klasser har någon som helst relation till Entity Framework förutom det faktumet att de har genererats baserat på en modell.

Nu har vi en modell och klasser som används av modellen. Det vi behöver göra nu för att kunna ställa frågor mot databasen med dessa klasser är att skapa en ObjectContext-klass för det.

Skapa en ny klass i Library.EntityRepository med namnet LibraryContext och se till att den ärver ObjectContext.

I ObjectContext-klassen så behöver vi dels en konstruktor som skapar upp ObjectSets för de olika entiteterna, samt properties för dem.

Den fullständiga klassen ser ut så här:

using System.Data.Objects;
using Library.Model;
 
namespace Library.EntityRepository
{
    public class LibraryContext : ObjectContext
    {
        private IObjectSet<Author> _authors;
        private IObjectSet<Book> _books;
 
        public LibraryContext()
            : base("name=LibraryEntitiesContainer", "LibraryEntitiesContainer")
        {
            _authors = CreateObjectSet<Author>();
            _books = CreateObjectSet<Book>();
 
            ContextOptions.LazyLoadingEnabled = true;
        }
 
        public IObjectSet<Author> Authors
        {
            get
            {
                return _authors;
            }
        }
 
        public IObjectSet<Book> Books
        {
            get
            {
                return _books;
            }
        }
    }
}

Förutom att skapa upp ObjectSet för Book och Author så har vi satt ContextOptions.LazyLoadingEnabled till true, för att vi ska slippa ladda in det själv när vi hämtar datan. Det gör att vi direkt kan få tag i till exempel Author.Books. Vi har även i konstruktorn ett anrop till konstruktorn som finns i basklassen (ObjectContext), där vi sätter namnet på vår connectionstring samt namnet på containern. Vad vi har för connectionstring kan du se i app.config om du är osäker.

Skapa ett program som använder modellen

Nu när vi har en modell, klasser för denna samt en ObjectContext så är nästa steg att skriva ett program där vi kan arbeta mot modellen, för att se så att vi får fram datan.

Innan vi skriver programmet så ska vi se till att ha någon data att hämta. Kör SQL-satsen nedan för att få in några exempel.

INSERT INTO [Library].[dbo].[Authors] (Name)
VALUES ('Astrid Lindgren')
 
INSERT INTO [Library].[dbo].[Authors] (Name)
VALUES ('H.C. Andersen')
 
INSERT INTO [Library].[dbo].[Books] ([AuthorId], [Title])
VALUES (1, 'Pippi Långstrump')
 
INSERT INTO [Library].[dbo].[Books] ([AuthorId], [Title])
VALUES (1, 'Bröderna Lejonhjärta')
 
INSERT INTO [Library].[dbo].[Books] ([AuthorId], [Title])
VALUES (2, 'Den fula ankungen')

Nu är vi redo att hämta datan, så öppna upp console-programmet och lägg in det här:

using System;
using Library.EntityRepository;
using Library.Model;
 
namespace Library.ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            using (LibraryContext ctx = new LibraryContext())
            {
                foreach (Author author in ctx.Authors)
                {
                    Console.WriteLine(author.Name);
 
                    foreach (Book book in author.Books)
                    {
                        Console.WriteLine("\t{0}", book.Title);
                    }
                }
            }
 
            Console.ReadKey(true);
        }
    }
}

Då vi för tillfället går mot den ObjectContext vi har i Library.EntityRepository så behöver vi en referens till det projektet, samt till System.Data.Entity.dll för att kunna köra koden.

Vi behöver även rätt connection string i det här projektet. Lättaste sättet att lösa det på är att dra App.Config från Library.EntityRepository till Library.ConsoleApplication. Då får vi en kopia på filen med rätt connection string.

Kör vi nu programmet så ser vi det här:

13 - Console

Vi kan hämta datan genom våra POCO-objekt nu, men vi har fortfarande en koppling mot Entity Framework i Console-programmet. För att få bort den så ska vi använda ett repository som vi kan hämta datan genom.

Skapa ett repository för att hämta datan

Vi börjar med att skapa ett interface för repositoryt. Det kommer att vara ett väldigt enkelt repository, men vi kan självklart lägga till andra metoder om vi önskar det.

Högerklicka på Library.Interfaces och välj Add -> New Item. Här väljer vi Interface och sätter namnet till IRepository.cs.

Vårt interface kommer att se ut så här:

using System;
using System.Collections.Generic;
 
namespace Library.Interfaces
{
    public interface IRepository<T>
    {
        T First(Func<T, bool> where);
        List<T> GetAll();
        void Add(T entity);
        void Delete(T entity);
    }
}

Det är ett väldigt enkelt och generiskt repository där vi anger vilken typ vi vill använda.

För att skapa en implementation av interfacet så skapar vi en ny klass i Library. Vi kallar filen för EntityRepository.cs. Det repositoryt vi kommer att använda för Entity Framework kommer även det att vara generiskt, så att vi kan välja vilken modell vi vill arbeta med.

Repositoryt i sin helhet ser ut så här:

using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Linq;
using Library.Interfaces;
 
namespace Library.EntityRepository
{
    public class EntityRepository<T> : IRepository<T> where T : class
    {
        IObjectSet<T> _objectSet;
 
        public EntityRepository()
        {
            _objectSet = new LibraryContext().CreateObjectSet<T>();
        }
 
        public T First(Func<T, bool> where)
        {
            return _objectSet.Where(where).FirstOrDefault();
        }
 
        public List<T> GetAll()
        {
            return _objectSet.ToList<T>();
        }
 
        public void Add(T entity)
        {
            using (var ctx = new LibraryContext())
            {
                _objectSet.AddObject(entity);
                ctx.SaveChanges();
            }
        }
 
        public void Delete(T entity)
        {
            using (var ctx = new LibraryContext())
            {
                _objectSet.DeleteObject(entity);
                ctx.SaveChanges();
            }
        }
    }
}

För att få det så generellt som möjligt mot Entity Framework så går vi inte direkt mot objekten i sig, utan skapar ett ObjectSet baserat på modellen. På så vis kan vi använda vilken modell vi vill.

För att testa det så ändrar vi i console-programmet till:

using System;
using Library.EntityRepository;
using Library.Interfaces;
using Library.Model;
 
namespace Library.ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            IRepository<Author> _authors = new EntityRepository<Author>();
 
            foreach (Author author in _authors.GetAll())
            {
                Console.WriteLine(author.Name);
 
                foreach (Book book in author.Books)
                {
                    Console.WriteLine("\t{0}", book.Title);
                }
            }
 
            Console.ReadKey(true);
        }
    }
}

Vi har inte längre någon referens till Entity Framework, utan jobbar enbart med objekten i vår modell. Då vi använder Irepository<T> så kan vi även enkelt byta ut repositoryt mot något annat, vilket kan vara lämpligt om vi skall skriva enhetstester.

För att testa hur väl det fungerar att byta ut repositoryt så skapar vi en ny implementation av Irepository<T> i Library.TestRepository. Vi kallar repositoryt för FakeRepository och gör det tämligen simpelt:

using System;
using System.Collections.Generic;
using System.Linq;
using Library.Interfaces;
using Library.Model;
 
namespace Library.TestRepository
{
    public class FakeRepository : IRepository<Author>
    {
        public Author First(Func<Author, bool> where)
        {
            return GetAll().Where(where).FirstOrDefault();
        }
 
        public List<Author> GetAll()
        {
            var authors = new List<Author>()
            {
                new Author()
                {
                    AuthorId = 1,
                    Name = "Nisse"
                },
 
                new Author()
                {
                    AuthorId = 2,
                    Name = "Mikael"
                }
            };
 
            return authors;
        }
 
        public void Add(Author entity)
        {
            //Gör ingenting nu
        }
 
        public void Delete(Author entity)
        {
            //Gör ingenting nu
        }
    }
}

Det vi har här är två hårdkodade författare som fortfarande inte har skrivit någon bok. Om vi ändrar i console-programmet från:

IRepository<Author> _authors = new EntityRepository<Author>();

Till:

IRepository<Author> _authors = new FakeRepository();

Så får vi det här när vi kör:

14 - FakeRepository Console 

Vi har nu gjort det möjligt att enkelt byta repository vid behov, och vi är inte heller knutna till Entity Framework.

Nästa steg blir att skapa ett ASP.NET MVC-program där vi tar nytta av funktionaliteten, samt utöver det använder AutoMapper för att mappa om modellerna till view models, samt använder Ninject för att enkelt kunna injicera ett repository till våra controllers från ett och samma ställe.

Använd repositoryt i ett ASP.NET MVC-projekt

I ASP.NET MVC kan vi enkelt injicera repositorys och annat till våra controllers. Då vi har ett ASP.NET MVC-projekt redo så skapar vi upp en ny controller i detta vid namn ”LibraryController”. Här skapar vi en konstruktor som tar emot IRepository<Author>, samt en default-konstruktor som sätter ett standard-repository (det används normalt av ASP.NET MVC).

Controllern ser nu ut så här:

using System.Web.Mvc;
using Library.EntityRepository;
using Library.Interfaces;
using Library.Model;
 
namespace Library.Mvc.Controllers
{
    public class LibraryController : Controller
    {
        private IRepository<Author> _authors;
 
        public LibraryController() : this(new EntityRepository<Author>()) { }
 
        public LibraryController(IRepository<Author> authorRepository)
        {
            _authors = authorRepository;
        }
 
        // GET: /Library/
        public ActionResult Index()
        {
            return View();
        }
    }
}

Det vi returnerar i Index-metoden kommer att vara en vymodell med namnet AuthorViewModel. Vi skapar därför upp en ny klass i Models-mappen med det namnet.

Vymodellen har properties för Id på författaren, namn på författaren samt antalet böcker som denne har skrivit.

namespace Library.Mvc.Models
{
    public class AuthorViewModel
    {
        public int AuthorId { get; set; }
        public string Name { get; set; }
        public int BooksCount { get; set; }
    }
}

Vi vill sedan hämta alla författare, samt mappa om till den här vymodellen.

Om du inte redan har gjort det, så lägg till referenser till AutoMapper.dll, Ninject.dll och Ninject.Web.Mvc.dll.

För att enkelt mappa om datan så använder vi AutoMapper. Vi skapar därför en ny klass, AutoMapperBootstrapper, där vi skapar upp mappningarna.

using System.Linq;
using AutoMapper;
using Library.Model;
using Library.Mvc.Models;
 
namespace Library.Mvc
{
    public class AutomapperBootstrapper
    {
        public static void Initialize()
        {
            Mapper.CreateMap<Author, AuthorViewModel>()
                .ForMember(viewmodel => viewmodel.BooksCount,
                author => author.MapFrom(a => a.Books.Count()));
        }
    }
}

Förutom att bara mappa om objektet här så sätter vi egenskapen BooksCount till antalet böcker som finns för den aktuella författaren.

I Application_Start i global.asax anropar vi Initialize(), vilket gör att vi kan använda oss utav mappningen i vår controller.

protected void Application_Start()
{
    AutomapperBootstrapper.Initialize();
    ...
}

För att mappa om objektet och returnera vår vymodell så lägger vi nu till följande i Index-metoden i controllern:

// GET: /Library/
public ActionResult Index()
{
    var authors = _authors.GetAll();
    var authorViewModel = Mapper.Map<List<Author>, List<AuthorViewModel>>(authors);
 
    return View(authorViewModel);
}

Nästa steg blir att skapa en vy för Index. Vi väljer att skapa en hårt typad vy mot Library.Mvc.Models.AuthorViewModel, samt väljer List.

Innan vi testar att starta projektet så se till att återigen lägga in rätt connection string i config-filen. Kopiera den befintliga från App.Config och klistra in under ConnectionStrings i web.config.

Surfar vi nu till sidan så får vi upp det här:

15 - Mvc List

Vi kan enkelt hämta data utan att vara kopplad mot Entity Framework på något sätt, samt har gjort det möjligt att enkelt ändra repository. Det finns dock ett problem – vi måste ange repository direkt i controllern. Vi vill istället använda dependency injection för att injicera rätt repository på alla ställen där vi använder Irepository<T>. Det finns väldigt många ramverk för det, men jag har valt Ninject då det på ett smidigt sätt låter oss göra det.

Använd dependency injection för att ange repository

Det är relativt enkelt att injicera kod in i våra controllers. Ninject som vi kommer att använda nu har ett plugin som låter oss göra det direkt i global.asax.

För att kunna göra det så måste vi först och främst ändra från HttpApplication till NinjectHttpApplication i global.asax. Vi ska sedan byta ut Application_Start mot Ninjects metod OnApplicationStarted.

protected override void OnApplicationStarted()
{
    AutomapperBootstrapper.Initialize();
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
}

När vi vill injicera kod med Ninject så kan vi välja en eller flera NinjectModule-klasser där vi har bundit olika interfaces till klasser. Det gör vi i en metod kallad CreateKernel som även den ligger i MvcApplication.

protected override IKernel CreateKernel()
{
    return new StandardKernel(new ServiceModule());
}

StandardKernel är en separat klass som vi har lagt i global.asax, men den kan lika gärna ligga i ett separat projekt om man så vill. Det är även möjligt att ladda in dessa dynamiskt.

internal class ServiceModule : NinjectModule
{
    public override void Load()
    {
        Bind<IRepository<Author>>().To<EntityRepository<Author>>();
    }
}

Här gör vi själva bindningen mellan Irepository<Author> och EntityRepository<Author>. På alla ställen där vi vill injicera Irepository<Author> kommer detta att slå igenom. Vi kan nu alltså ta bort default-konstruktorn från vår controller:

using System.Collections.Generic;
using System.Web.Mvc;
using AutoMapper;
using Library.Interfaces;
using Library.Model;
using Library.Mvc.Models;
 
namespace Library.Mvc.Controllers
{
    public class LibraryController : Controller
    {
        private IRepository<Author> _authors;
 
        public LibraryController(IRepository<Author> authorRepository)
        {
            _authors = authorRepository;
        }
 
        // GET: /Library/
        public ActionResult Index()
        {
            var authors = _authors.GetAll();
            var authorViewModel = Mapper.Map<List<Author>, List<AuthorViewModel>>(authors);
 
            return View(authorViewModel);
        }
    }
}

Byter vi ut EntityRepository<Author> mot FakeRepository så kommer vi att använda FakeRepository på alla ställen istället.

Sammanfattning

Vi har nu skapat en ASP.NET MVC-sida där vi enkelt kan injicera valfritt repository, samt även automatiskt mappar om detta till en vymodell. Hela lösningen är väldigt skalbar, och blir dessutom enkel att testa. Vi skulle även utöver detta kunna skapa andra typer av repositorys och mappningar mot andra objekt om vi skulle vilja det, och då göra dessa lika skalbara med hjälp av den funktionaliteten vi redan har.

Artikeln som pdf och xps, samt exempel koden finns att ladda ned här:
http://cid-4aa13e17331c7398.office.live.com/browse.aspx/Public/ASP.NET/POCO%20med%20Entity%20Framework

7 Comments

  • För större projekt blir det inte lättare att ha ett repository för tex Authors och om förlag om man skulle vilka ha det? Tänker mest på om man vill ha mer komplexa frågor mot databasen.

  • Tack för en bra artikel, POCO stödet kände ja inte till, verkar klar smidigt!

  • @Joakim Westin
    Tack själv! POCO-stödet är nytt i .NET 4.0, och är du intresserad av mer info om det så kan jag rekommendera att du spanar lite på Dag könig på Microsoft. Han har hållt en del dragningar om hur man kan arbeta med Entity Framework på olika sätt på bland annat Swenug (http://www.swenug.se).

    Jag kan även rekommendera att du kikar på det här om du är intresserad av Entity Framework:
    http://buzzfrog.blogs.com/zabrak/2010/06/heldag-med-julie-lerman-om-entity-framework-den-24-juni.html

  • Tack för en intressant artikel!

    Länken till exemplekoden fungerar inte för mig, det står att objektet kanske är borttaget eller flyttat. Finns projektet möjligen att få tag på någon annanstans?

    Hälsningar,
    Roger Hansson

  • Mycket intressant och bra artikel. Tänkte dock på EntityRepository klassen där du inte går direkt mot objekten utan gör den generisk. Det är asbra, men det förutsätter väl att LibraryContext modellen innehåller hela databasen.

    Tänkte att om databasen är större och man kanske väljer att ha en modell för varje entity, hur skulle man då kunna göra så att man även kan välja vilken modell man kör på i EntityRepository klassen? För då kan man ju använda den som basklass.

  • Anv 228 nd poco st 246 det i entity framework 4.. Reposted it :)

  • Anv 228 nd poco st 246 det i entity framework 4.. Super :)

Comments have been disabled for this content.