Model View Controller
Model View Controller
I. Introducere
I.1 Arhitectura MVC
Model View Controller este un șablon arhitectural extrem de utilizat în ingineria software. Scopul acestui model este de încuraja separarea dintre diferitele pǎrți ale aplicației, în conformitate cu șablonul Separation of Concerns din ingineria programǎrii. Separation of Concerns este un principiu de bazǎ al dezvoltǎrii de platforme și framework-uri.
O consecințǎ a acestei arhitecturi este obținerea unor module slab cuplate (loosely-coupled) cu beneficii atât pe termen lung cât și scurt vizavi de ciclul de viața al aplicației, și anume:
componentele individuale nu depind în mod direct de alte component ceea ce permite dezvoltarea și/sau înlocuirea independentǎ a acestora (in isolation)
componentele dezvoltate într-o manierǎ izolatǎ implicǎ și faptul cǎ schimbǎrile care survin în cadrul acestora au implicații limitate la un numǎr relative redus de alte component, ceea ce conduce la o întreținere mai simplǎ
testarea este de asemenea mult mai simplǎ datoritǎ cuplajului redus între componente
Figura 1: Arhitectura Model-View-Controller
MVC nu este un concept nou, el a fost prezentat în 1979 de Trygve Reenskaug care lucra la laboratoarele de cercetare Xerox PARC și utilizat în aplicații care utilizau limbajul de programare SmallTalk .
Șablonnul MVC este utilizat și de o multitudine de framework-ui GUI (graphical user interface) cum ar fi: Ruby on Rails (pentru limbajul Ruby), Apple Cocoa (oferit de Apple pentru crearea aplicațiilor Mac OS și IOS), Apache Struts (folosit în programarea web pentru limbajul Java), Zend, Laravel, Simphony și altele (pentru limbajul PHP) și ASP.NET (oferit de Microsoft).
Acest șablon separă aplicația în trei mari componente semantice și funcționale:
Model – obiectele model sunt părți ale aplicației ce implementează logica pentru domeniul de date al aplicației (comportament, validǎri); datele sunt reprezentate în general de atributele de date sau proprietǎtile asociate acestora iar logica asociatǎ acestora se referǎ în general la asigurarea persistenței. Adesea, obiectele model regăsesc și memorează starea modelului într-o bază de date dar acest lucru nu este neapǎrat necesar
View – view-urile sau vizualizǎrile sunt componente care controleazǎ modul în care elemente din interfeței cu utilizatorul (UI) sunt afișate ( prezentarea cǎtre utilizator) – se ocupǎ de randarea interfeței (HTML, CSS) și realizeazǎ legǎtura cu modelul. În mod obișnuit acestea conțin textbox-uri, combobox-uri, listview-uri etc. Logica UI aparține nivelului vizualizare
Controller – controller-urile sunt elementele care recepționeazǎ intrarea de la utilizator, controleazǎ logica aplicației, interacționează cu modelul pentru a realiza acțiuni specifice și selectează o vizualizare pentru a afișa rezultatele. Practic acționeazǎ ca un coordonator între model și vizualizare (view) și coordoneazǎ comunicarea între model și nivelul de date
Aceastǎ arhitecturǎ a aplicațiilor bazatǎ pe separarea nivelurilor specifice șablonului MVC separare aduce un plus de flexibilitate prin faptul cǎ fiecare componentǎ a aplicației poate fi dezvoltatǎ, modificatǎ și testatǎ separat. Chiar dacǎ fiecare componentǎ individualǎ are responsabilitǎți distincte, ele interacționeazǎ unele cu altele; prin decuplarea însǎ a interfeței de logica aplicației, arhitecturile MVC sunt scalabile și ușor mentenabile.
MVC este un șablon de nivel înalt, având diverse implementǎri și diferite sub-șabloane adiacente.
I.2. MVC în aplicațiile web
Șablonul original MVC a fost proiectat pornind de la ideea cǎ modelul, controller-ul și vederea aparțin toate aceluiași context. În varianta originalǎ controller-ul utilizeazǎ șablonul observer pentru a monitoriza schimbǎrile din view și a reacționa la intrǎrile utilizatorului.
Deoarece aplicațiile web sunt fǎrǎ stare iar vederile (view) ruleazǎ la client în cadrul unui browser așa încât șablonul Observer nu este utilizabil în acest context, el fiind înlocuit cu șablonul Front Controller: atunci când o cerere este primitǎ, un controller o recepționeazǎ și o proceseazǎ iar rezultatul e trimis înapoi cǎtre client. Forța acestui șablon devine evidentǎ atunci când aceeași acțiune a unui controller trebuie executatǎ dar trebuie returnat conținut diferit pentru fiecare cerere ținându-se cont daca a fost o cerere normal a browser-ului (se returneazǎ întreaga paginǎ cu layout, stil, script-uri, etc.) sau dacǎ a fost o cerere AJAX (se returneazǎ doar o vedere parțialǎ).
În plus, într-o aplicație web componentele trebuie proiectate astfel încât sǎ utilizeze abstractizǎri pentru comunicarea dintre ele în scopul de a izola anumite funcționalitǎți sub forma unor servicii care suportǎ o abordare modularǎ de tipul plug-and-play. Exemple de astfel de situații le constituie aspetele legate de securitate, autentificare și logare, caching, astfel încât acestea sǎ poatǎ fi modificate fǎrǎ impact asupra altor module ale aplicației. Majoritatea framework-urilor MVC sunt exensibile permițând includerea de componente diverse într-o manierǎ foarte simplǎ.
O altǎ problemǎ importantǎ în proiectarea unei aplicații web o reprezintǎ validarea: la ce nivel trebuie realizatǎ în contextul aplicǎrii șablonului MVC care, teoretic, promoveazǎ ideea cǎ modelul ar trebui sǎ fie responsabil de aceastǎ funcționalitate. Totuși, într-o aplicație structuratǎ pe mai multe niveluri, fiecare nivel ar trebui sǎ dispunǎ de un anumit grad de responsabilitǎți în acest sens. Astfel, de exemplu, la nivel de client, JavaScript (JQuery) trebuie utilizatǎ pentru a verifica câmpurile obligatorii și a restricționa intrarea din partea utilizatorului din punctual de vedere al tipului de date introdus; nivelul de logicǎ a aplicației trebuie sǎ forțeze toate regulile de validare necesare iar nivelul de date (baza de date) este responsabil cu prevenirea violarii constrângerilor din baza de date.
Nu în ultimul rând, în scopul îmbunǎtǎțirii performanțelor aplicațiilor web sunt important de avut în vedere mecanismele de caching, atât pe partea de server cât și pe partea de client; astfel, nivelurile client și server din arhitectura MVC pun la dispoziție tehnici specifice pentru realizarea cahing-ului la nivelul aplicației web, atât la nivel de client cât și de server.
Nu în ultimul rând, procesul de dezvoltare a aplicațiilor web trebuie sǎ adere la bunele practici ale proiectǎrii orientate pe obiecte, promovând reutilizarea și exensibilitatea: SOLID, DRY.
SOLID – descrie un set de principii de proiectare și dezvoltare a unei aplicații. Aceste principii sunt urmǎtoarele:
1. Single Responsibility Principle (SRP) – obiectele trebuie sǎ aibe o singurǎ responsabilitate și întregul comportament implementat în cadrul unui obiect trebuie sǎ se focuseze pe acea responsabilitate. Implementarea trebuie sǎ aibǎ în vedere creare de clase/obiecte specializate și nu obiecte care fac de toate
2. Open/Closed Principle (OCP) – componentele trebuie sǎ fie deschise pentru schimbare dar închise pentru modificǎri: noile component (clase) se obțin prin derivarea componentelor ințiale și nu prin modificarea acestora. Pentru generalitate și flexibilitate se utilizeazǎ în general abstracții pe baza cǎrora se creazǎ noile componente diferite, prin derivare. Complementeazǎ principiul anterior SRP
3. Liskov Substitution Principle (LSP) – orice obiect trebuie sǎ poatǎ fi înlocuit (substituit) cu instanțe de tipuri derivate din acesta (sub-tipuri) fǎrǎ implicații asupra altor obiecte. De obicei, acest lucru implicǎ existența a unei clase de bazǎ/interfețe comune.
4. Interface Segregation Principle (ISP) – în locul creǎrii unei singure interfețe care sǎ conținǎ tot comportamentul dorit de la un obiect, se preferǎ crearea mai multor interfețe, mai mici și cu un comportament mai specific pe care obiectul sǎ le implementeze. O Atfel de abordare limiteazǎ dimensiunile interfețelor de-a lungul aplicației și promoveazǎ utilizarea mai multor interfețe, mult mai specializate.
5. Dependency Inversion Principle (DIP) – componentele care depind una de cealaltǎ nu trebuie sǎ interacționeze prin intermediul unei implementǎri concrete, ci prin intermediul unor abstracții. Astfel, componentele pot sa fie dezvoltate și schimbate în mod independent. O abordare cunoscutǎ în acest sens o reprezintǎ utilizarea pattern-ului Repository, des întâlnit și utilizat în aplicațiile web MVC, care introduce un nivel de abstractizare între controller și date pentru a facilita implementarea persistenței și testarea.
Principiile SOLID sunt complementate de un alt principiu de proiectare des întâlnit și utilizat: DRY – Don’t Repeat Yourself. Acesta se referǎ la evitarea duplicarea codului identic sau similar în cadrul aplicațiilor prin crearea de de cod reutilizabil.
I.3. ASP.NET MVC
Soluția ASP.NET MVC de la Microsoft reprezintǎ o alternativǎ fațǎ de dezvoltarea axatǎ pe formulare web reprezentatǎ de ASP.NET Web Forms. Din martie 2012 ASP.NET MVC este open-source, întregul cod sursǎ pentru framework-ul ASP.NET MVC, Web API și Web Pages este disponibil pentru vizualizare și descǎrcare pe CodePlex http://aspnetwebstack.codeplex.com/ . Prima versiune ASP.NET MVC 1 a apărut în 2009, MVC 2 în 2010, MVC 3 în 2011, MVC 4 în 2012, iar în 2013 a apărut MVC 5.
Dezvoltarea framework-ului ASP.NET MVC respectǎ principiile de bazǎ și bunele practici ale proiectǎrii orientate pe obiecte, promovând reutilizarea și exensibilitatea: SOLID, DRY:
Astfel, întreaga manierǎ de organizare la nivelul structurii claselor controller/view (și nu numai!) in ASP.NET MVC respectǎ principiul Single Responsibility Principle (SRP), clasele fiind focalizate pe responsabilitǎți specifice. De asemenea, întreaga arhitecturǎ se bazeazǎ pe clase abstracte pe baza cǎrora se creazǎ noile componente diferite, prin derivare – de exemplu, toate clasele controller derivate din Controller, în conformitate cu principiul Open/Closed Principle (OCP).
Obiectele care au o clasǎ /interfața de bazǎ comunǎ sunt substituibile – Liskov Substitution Principle (LSP) iar modalitatea în care sunt construite și utilizate intefețele în .NET respectǎ principiul Interface Segregation Principle (ISP) – interfețe de dimensiuni mici, specializate: ISerializable, IDisposable, etc.
De asemenea, în ASP.NET MVC se încurajeazǎ crearea unor nivele suplimentare de abstractizare între componente, o abordare cunoscutǎ în acest sens fiind reprezentatǎ de utilizarea pattern-ului Repository – un nivel de abstractizare între controller și date pentru a facilita implementarea persistenței și testarea, în concordanțǎ cu principiul Dependency Inversion Principle (DIP).
Framework-ul ASP.NET MVC oferǎ diferite modalitǎți de a adera la acest principiu DRY, una dintre aceasta fiind reprezentatǎ de utilizarea filtrelor de acțiune (action filter) implementate utilizând atribute .NET. Filtrele furnizeazǎ o manierǎ declarativǎ de a adǎuga prelucrǎri pre/post metodelor acțiune din cadrul unui controller; prelucrǎrile care se repetǎ pot fi implementate o singurǎ datǎ în cadrul unui filtru și aplicate de câte ori se dorește (DRY).
Ahitectura generalǎ a unei aplicații web ASP.NET MVC respectǎ principiile generale ale dezvoltǎrii unei aplicații web bazatǎ pe șablonul MVC, fiind compusǎ din trei niveluri distincte, fiecare dintre acestea dispunând de o serie de componente specifice. Funcționalitǎțile cross-nivel sunt separate în diferite servicii ale aplicației pe nivelurile corespunzǎtoare.
Figura 2: Ahitectura generalǎ a unei aplicații web ASP.NET MVC
1. Nivelul client (browser) – ruleazǎ în cadrul browser-ului și realizeazǎ cereri HTTP pentru a regǎsi conținut HTML sau executǎ cereri Ajax
HTML/CSS – elementele interfeței cu utilizatorul utilizate pentru descrierea aspectului (layout) și stilului aplicației
JavaScript – conține logica de validare la nivel de client
Securitate – include elementele de securitate la nivel de client (cookies)
Logare – serviciu local utilizat pentru logare
Local Storage – Local Storage HTML 5
Browser Cache – Cache furnizat de browser utilizat pentru stocarea diferitelor elemente la nivel de client: HTML, CSS, imagini, etc.
2. Nivelul serverului web – include framework-ul ASP.NET MVC și ansamblele aplicației
Vederile (View) – utilizate pentru a randa conținut HTML
Model – clase reprezentând modelul domeniului pentru aplicație
Controller – clasele controller-ele aplicației; include validare intrǎri și coordonare între model și view
Securitate – servicii de securitate utilizat pentru autentificare și autorizare utilizatori
Logare – serviciu utilizat pentru logare
Repository – componente pentru accesul la date (Object Relational Mapper)
Service Layer – serviciu care încapsuleazǎ procese complexe ale aplicației și de gestionare a persistenței
Session State/Caching – serviciu pentru managementul stǎrilor aplicației
Monitoring – serviciu pentru monitorizare a performanțelor aplicației
3. Nivelul de date – include baza de date (relaționalǎ sau non-relaționalǎ) și/sau alte servicii externe sau API-uri de care depinde aplicația (REST/SOAP API)
Aplicațiile ASP.NET MVC pot sǎ rezide același ansamblu cu web-site-ul sau se pot separa componentele în mai multe ansamble, diferit de website. Indiferent de abordare, se va utiliza un spațiu de nume comun tuturor ansamblelor.
I.4. MVC și structura aplicațiilor ASP.NET MVC
Din punctul de vedere al implementǎrii, conceptul care stǎ la baza arhitecturii MVC și care oferǎ productivitate și flexibilitate în dezvoltarea aplicațiilor este așa numitul convention over configuration. Acesta implicǎ faptul cǎ, în loc de a baza întreaga dezvoltare a aplicației pe niște setǎri bazate pe configurǎri explicite, arhitectura MVC se bazeazǎ pe niște convenții la care toți dezvoltatorii trebuie sǎ adere, atât în ceea ce privește denumirile atribuite directoarelor și fișierelor cât și în ceea ce privește amplasarea acestora în cadrul proiectului. Întregul mod de organizare și structurare a aplicațiilor MVC reprezintǎ un exemplu în acest sens.
Crearea unei aplicații ASP.NET MVC se realizeazǎ prin selectarea template-ului corespunzǎtor din Visual Studio: ASP.NET web Application/MVC.
Atunci când se creazǎ un proiect de tipul ASP.NET MVC în Visual Studio, structura proiectului include un numǎr și tip specific de directoare și subdirectoare. Astfel, convențiile cele mai importante sunt urmǎtoarele:
existǎ un subdirector dedicat, specific, pentru fiecare nivel din arhitectura MVC: Controllers, Models, Views
toate clasele controller au sufix-ul Controller – framework-ul se bazeazǎ pe acest sufix pentru a înregistra cotroller-ele aplicației la pornire și a realiza asocierea acestora cu rutele corespunzǎtoare. Într-o aplicație de tipul ASP.NET MVC se creazǎ implicit controller-ele Home și Account
directorul Views are un subdirector Shared corespunzǎtor vederilor care sunt împǎrțite (shared) între mai multe controller-e; vederile asociate unui anumit controller se grupeazǎ, în subdirectoare separate cu numele corespunzator controller-ului asociat. Se creazǎ implicit directoare specifice pentru vederile associate controller-elor create implicit în cadrul structurii (Home și Account) și, de asemenea, se mai creeazǎ un director pentru vederile utilizate în comun în cadrul aplicației (Shared)
directorul Models include modelele aplicației – poate include clase model create în procesul de dezvoltare al aplicației sau se poate utiliza Entity Framework (dacǎ e cazul) pentru generarea modelului. Implicit, template-ul MVC creazǎ modelul AccountModels.
Directorul App_Start conține fișiere de configurare a aplicației (de exemplu pentru rutare, fișierul RouteConfig, pentru configurarea bundle-urilor BundleConfig, a maniereri de autorizare AuthConfig, etc.)
Directorul Scripts include fișiere JavaScript; implicit sunt incluse aici fișiere aferente jQuery și alte script-uri utilizate de MVC
Existǎ mai multe variante de creare a unei aplicații printre care crearea manualǎ a claselor model sau, opțiunea cea mai simplǎ, generarea automatǎ a modelului din baza de date (care se presupune cǎ existǎ deja) utilizând Entity Framework.
Astfel, vom utiliza pentru exemplificare o aplicație destinatǎ evidenței studenților dintr-o universitate denumitǎ MyUniversityApp. În cadrul aplicației au fost create 5 controller-e (AccountController, HomeController, CourseController, EnrollmentController, StudentController) iar vederile asociate au fost grupate corespunzǎtor (directoare Home, Accout, Student, Enrollment, Course). Figura 3 prezintǎ structura fișierelor generate în cadrul proiectului.
Figura 3: Structura fișierelor în cadrul unui proiect ASP.NET MVC
De asemenea, modelul de date al aplicației se bazeazǎ pe Entity Framework (MyUniversity.edmx) și a fost generat utilizând o bazǎ de date existentǎ preluatǎ din http://www.asp.net/mvc/tutorials/mvc-5/database-first-development/setting-up-database.
Figura 4: Structura fișierelor model generate
Baza de date conține 3 tabele: Courses, Students, Enrollments. Modelul generat utilizând Entity Framework a fost creat cu opțiunea Add/ADO.NET Entity Data Model – opțiunea Generate form DataBase. Modelul MyUniversity.edmx poate fi vizualizat în Designer și include clasele generate pe baza tabelelor din baza de date împreunǎ cu relațiile dintre acestea (Figura 5):
Figura 5: Modelul generat vizualizat în Entity Designer
II. Elemente fundamentale ASP.NET MVC
II.1. Rutarea în MVC
Una dintre diferențele fundamentale dintre ASP.NET și ASP.NET MVC îl reprezintǎ modalitatea în care se realizeazǎ corespondența dintre URL și pagina care va fi afișatǎ în browser. Astfel, în ASP.NET Web Forms există o corespondență directǎ între URL și pagină, pentru fiecare pagină apelându-se fișierul *.aspx corespunzător. În ASP.NET MVC cererile din browser sunt mapate cǎtre acțiuni din cadrul unui controller și nu cǎtre o paginǎ anume. Corespondența dintre URL și pagina afișatǎ în acest caz are la bazǎ un mecanism de rutare care nu este altceva decât un mecanism de corespondențe (pattern matching system). Chiar dacǎ nu este menționat nicǎieri prin nume, framework-ul de rutare ASP.NET reprezintǎ pilonul central al fiecǎrei cereri ASP.NET MVC.
Pentru implementarea mecanismului de rutare se folosește o tabelă de rutare pentru a trata cererile ce apar. Rutarea din ASP.NET este folosită și de ASP.NET MVC. La pornire, aplicația înregistreazǎ unul sau mai multe șabloane de rutare (patterns) în tabela de rutare astfel încât sistemul de rutare sǎ poatǎ trata corespunzǎtor fiecare cerere care corespunde șablonului respectiv. Metoda folosită este RegisterRoutes în care se adaugă o intrare în RouteCollection folosind metoda MapRoute. Un exemplu este urmǎtorul (fișierul App_Start/RouteConfig.cs):
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index",
id = UrlParameter.Optional }
);
}
}
Astfel, se creazǎ o rutǎ numitǎ Default care va gestiona cererile de forma adresa_site/nume_controller/actiune_controller/parametru. Dacǎ anumite elemente din cadrul rutei nu sunt specificate se vor utiliza valorile implicite definite la secțiunea defaults, ca de exemplu pentru http://localhost:58269/ se va apela metoda acțiune Index din cadrul controller-ului Home.
Apelul metodei RegisterRoutes se plasează în Global.asax.cs în cadrul apelului metodei Application_Start:
protected void Application_Start()
{
…
RouteConfig.RegisterRoutes(RouteTable.Routes);
…
}
Se observǎ cǎ o rută este formată din:
Nume – utilizat pentru a o referi în mod specific
URL cu parametri: “{controller}/{action}/{id}” – definește șablonul ; de exemplu un URL de genul /Student/Details/5
Parametrii impliciți dați sub forma unui obiect (opționali):
new { controller = “Home”, action = “Index”, id = UrlParameter.Optional }
Șabloanele pentru URL sunt relative fațǎ de rǎdǎcina (root) a aplicației și nu sunt
case-sensitive. Modul de funcționare a rutǎrii în ASP.NET MVC este prezentat în Figura 6:
Figura 6: Rutarea în ASP.NET MVC
utilizatorul face o cerere pentru unele resurse de pe server (prin introducerea unei adrese URL în browser)
când motorul de rutare (Routing Engine) primește cererea, o parseazǎ și apoi verificǎ dacǎ ea se încadreasǎ în șabloanele deja înregistrate pentru a localiza pagina doritǎ
dacǎ cererea este ok, se proceseazǎ și se returneazǎ pagina în browser, dacǎ nu, eroare
Mecanismul de rutare și motoarele de afișaj (view engines) contribuie în mod determinant la procesarea cererilor HTTP și implementarea șablonului Front Controller care stǎ la baza MVC. Atunci când se primește o cerere (student/details/13) ASP.NET MVC cautǎ în tabela de rutare și invocǎ acțiunea corespunzǎtoare a controller-ului. Controller-ul trateazǎ cererea și determinǎ ce tip de valoare sǎ returneze și deleagǎ cǎtre motorul de afișaj sarcina de a încǎrca și de a randa vizualizarea (View).
Figura 7: Mecanismul de rutare în ASP.NET MVC
Modul în care se utilizeazǎ conceptul convention over configuration este pregnant ilustrat în cadrul mecanismului de rutare. La pornirea aplicației framework-ul de ASP.NET MVC descoperǎ toate controller-ele aplicației cǎutând printre ansamblele disponibile clase care implementeazǎ interfața System.Web.Mvc.IController și se terminǎ cu sufix-ul Controller. În cadrul fiecǎui controller, se determinǎ acțiunile specifice acestuia – metodele acțiune din cadrul clasei controller care sunt implicate în procesarea cererii și conțin efectiv logica de program necesarǎ acestei procesǎri.
Sistemul de rutare din MVC utilizeazǎ șablonul Factory pentru rezolvarea cererilor de rutare – așa numitul Controller Factory, care este responsabil de crearea instanțelor specifice a claselor controller. Modul în care se ruteazǎ cererea și se mapeazǎ cǎtre un anumit controller specific implicǎ urmǎtoarea cale:
Cerere -> Sistemul de rutare -> Controller Factory -> Controller
Controller Factory primește cererea și o mapeazǎ cǎtre un anumit controller specific; clasa principalǎ în implementarea șablonului este DefaultControllerFactory, implementeazǎ IControllerFactory. Instanța creatǎ a clasei DefaultControllerFactory este responsabilǎ cu crearea efectivǎ clasei controller utilizând metoda CreateController. Metoda are 2 parametri: contextul cererii reprezentat prin RequestContext și numele controller-ului ca și un string; ea returneazǎ instanța creatǎ a unui controller specific. Conform convențiilor specifice MVC, numele clasei controller create va avea sufix-ul Controller.
II.2. Nivelul Controller – Clasele controller
Toate clasele Controller sunt localizate în directorul Controllers al aplicației. O structurǎ tipicǎ de controller se gǎsește în HomeController.cs:
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "Welcome to MyUniversity site!";
return View();
}
public ActionResult About()
{
ViewBag.Message = "About MyUniversity application";
return View();
}
public ActionResult Contact()
{
ViewBag.Message = "Contact page";
return View();
}
}
Acesta conține trei metode acțiune sau, pe scurt, acțiuni: Index, About și Contact. Deci, pornind de la ruta implicit definitǎ prin șablonul {controller}/{action}/{id}, un URL de forma http://localhost:58269/Home/About va determina faptul cǎ este vorba de controller-ul HomeController și metoda – acțiune About din cadrul acestuia care se va executa.
Dupǎ execuția unei anumite acțiuni din cadrul unui controller, e important sǎ se transmitǎ mai departe ce trebuie executat în continuare; de exemplu, dacǎ trebuie afișate niște rezultate în cadrul unei vizualizǎri (view). Rezultatul redat de o metodǎ acțiune rezultǎ în general într-o instanțiere a unei clase derivate din ActionResult: comunicarea controller-view se realizeazǎ utilizând ActionResult, valoarea returnatǎ pe care fiecare acțiune a unui controller este așteptatǎ sǎ o returneze. De obicei însǎ, pentru a fi mult mai specifici în ceea ce privește rezutatul returnat de cǎtre o anumitǎ acțiune a unui controller, se utilizeazǎ clase derivate din ActionResult. Tipul returnat astfel se obține în urma apelului unor metode helper din clasa System.Web.Mvc.Controller:
ContentResult – prin intermediul metodei helper Content() se returneazǎ un conținut – se randeazǎ în view ca un simplu text
FileResult – prin intermediul metodei helper File() – randeazǎ conținutul unui fișier (de exemplu, .pdf)
JavaScriptResult – prin intermediul metodei helper JavaScript() – pentru JavaScript
JsonResult – prin intermediul metodei helper Json() – serializeazǎ un obiect și îl randeazǎ în format JSON
PartialViewResult – prin intermediul metodei helper PartialView() – randeazǎ numai conținutul unui view, nu și layout-ul acestuia
RedirectResult – prin intermediul metodei helper RedirectResult() – redirecteazǎ user-ul cǎtre un alt URL dat; RedirectToAction() și RedirectToRoute() sunt similare doar cǎ în cazul acestora framework-ul determinǎ în mod dynamic URL-ul extern
ViewResult – prin intermediul metodei helper View() – returneazǎ un ViewResult care se radeazǎ ca o vizualizare (view)
Clasele controller implementeazǎ interfața IController și sunt derivate din System.Web.Mvc.Controller. Adǎugarea unui nou controller din Visual Studio se realizeazǎ prin comanda Add/Controller lansatǎ din spațiul directorului Controller din cadrul proiectului. Implicit, la selectarea unei aplicații de tipul ASP.NET MVC, proiectul conține inițial douǎ clase controller: HomeController și AccountController. Crearea controller-elor este asistatǎ în Visual Studio putându-se alege dintre diferite tipuri de controller-e: API sau MVC, fiecare dintre acestea putând fi în forma cea mai simplǎ (Empty MVC sau Empty API), MVC sau API Controller with empty read/write actions, MVC/API Controller with read/write actions using Entity Framework.
Figura 8: Adǎugarea unui controller
Atunci când se selecteazǎ una dintre opțiunile care genereazǎ automat metode-acțiune de tipul read/write, se poate observa faptul cǎ metodele generate Create, Edit și Delete sunt supraîncǎcate, cea de-a doua implementare a metodelor fiind decoratǎ cu atributul [HttpPost]. Acest lucru este necesar deoarece acțiunile Create, Edit și Delete implicǎ douǎ cereri pentru a se finaliza: prima cerere returneazǎ vederea (view-ul) cu care interecționeazǎ utilizatorul și cea de-a doua realizeazǎ efectiv operația doritǎ (creare/editare/ștergere date).
De exemplu, pentru aplicația MyUniversityApp, au fost implementate încǎ trei clase controller: StudentController, CourseController și respectiv EnrollmentController.
Figura 9: Directorul Controllers
Implementarea metodelor este dependentǎ de modelul utilizat pentru date: dacǎ se utilizeazǎ Entity Framework, implementarea claselor Controller va utiliza o instanțǎ a contextului creat (vezi cap. II.4 Nivelul Model – Clasele Model – DataBase First):
DBUniversityEntities db = new DBUniversityEntities()
,în cadrul cǎreia proprietǎțile abstratizeazǎ printr-un DbSet tabelele din baza de date: db.Students, db.Enrollments, db.Courses. Aceastǎ opțiune apare și trebuie selectatǎ ca și model (Model class) la crearea controller-ului. Entitǎțile individuale sunt reprezentate de clasele model Student.cs, Enrollment.cs și respectiv Course.cs a cǎror implementare se gǎsește în directorul Models .
Astfel, o implementare a clasei ControllerStudent în aceastǎ abordare este urmǎtoarea :
using MyUniversityApp.Models;
namespace MyUniversityApp.Controllers
{
public class StudentController : Controller
{
private DBUniversityEntities db = new DBUniversityEntities();
//
// GET: /Student/
public ActionResult Index()
{
return View(db.Students.ToList());
}
//
// GET: /Student/Details/5
public ActionResult Details(int id = 0)
{
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
//
// GET: /Student/Create
public ActionResult Create()
{
return View();
}
//
// POST: /Student/Create
[HttpPost]
public ActionResult Create(Student student)
{
//aplicam un bloc try – daca nu se pot salva datele
try
{
if (ModelState.IsValid)
{
db.Students.Add(student);
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException)
{
//inregistreaza eroarea in ModelState
ModelState.AddModelError("", "Nu se pot salva modificarile!");
}
return View(student);
}
//
// GET: /Student/Edit/5
public ActionResult Edit(int id = 0)
{
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
//
// POST: /Student/Edit/5
[HttpPost]
public ActionResult Edit(Student student)
{
if (ModelState.IsValid)
{
db.Entry(student).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(student);
}
//
// GET: /Student/Delete/5
public ActionResult Delete(int id = 0)
{
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
//
// POST: /Student/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
Student student = db.Students.Find(id);
db.Students.Remove(student);
db.SaveChanges();
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
Fiecare metodǎ-acțiune a controller-ului returneazǎ o vedere specificǎ; identificarea vederii returnate se realizeazǎ pe baza numelui/directorului în care aceasta se localizeazǎ. Una din convențiile ASP.NET MVC precizeazǎ faptul cǎ toate vederile trebuie sǎ rezide în directorul Views al aplicației, grupate pe subdirectoare cu numele identic cu cel al controller-ului cǎruia îi este asociatǎ vederea respectivǎ. De exemplu, vederea returnatǎ de metoda-acțiune Index se gǎsește în directorul Views, subdirectorul Student (deoarece aparține controller-ului StudentController), în fișierul Index.cshtml (numele metodei acțiune care îi este asociatǎ). Urmând acest principiu, localizarea vederii care trebuie returnate este foarte ușor de realizat. Evident, vederea respectivǎ care este returnatǎ de acțiunea controller-ului trebuie sǎ existe (trebuie creatǎ). Existǎ și cazul în care se utilizeazǎ metoda RedirectToAction prin care se trasferǎ controlul cǎtre o altǎ acțiune: de exemplu, în cadrul metodei DeleteConfirmed, cǎtre acțiunea Index.
Metodele acțiune care trateazǎ datele transmise cǎtre browser sunt adnotate în mod explicit cu atributul [HttpPost]. Restul metodelor acțiune sunt cele care doar rǎspund cererilor de resurse solicitate de browser cǎrora le-ar corespunde atributul [HttpGet], atribut implicit care nu necesitǎ includerea sa explicitǎ în cod.
Figura 10: Directorul Views
Clasa CourseController are o structurǎ similarǎ:
using MyUniversityApp.Models;
namespace MyUniversityApp.Controllers
{
public class CourseController : Controller
{
private DBUniversityEntities db = new DBUniversityEntities();
//
// GET: /Course/
public ActionResult Index()
{
return View(db.Courses.ToList());
}
//
// GET: /Course/Details/5
public ActionResult Details(int id = 0)
{
Course course = db.Courses.Find(id);
if (course == null)
{
return HttpNotFound();
}
return View(course);
}
//
// GET: /Course/Create
public ActionResult Create()
{
return View();
}
//
// POST: /Course/Create
[HttpPost]
public ActionResult Create(Course course)
{
if (ModelState.IsValid)
{
db.Courses.Add(course);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(course);
}
//
// GET: /Course/Edit/5
public ActionResult Edit(int id = 0)
{
Course course = db.Courses.Find(id);
if (course == null)
{
return HttpNotFound();
}
return View(course);
}
//
// POST: /Course/Edit/5
[HttpPost]
public ActionResult Edit(Course course)
{
if (ModelState.IsValid)
{
db.Entry(course).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(course);
}
//
// GET: /Course/Delete/5
public ActionResult Delete(int id = 0)
{
Course course = db.Courses.Find(id);
if (course == null)
{
return HttpNotFound();
}
return View(course);
}
//
// POST: /Course/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
Course course = db.Courses.Find(id);
db.Courses.Remove(course);
db.SaveChanges();
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
Se pot adǎuga și funcționalitǎți de cǎutare în cadrul metodelor controller-ului: de exemplu, dacǎ se dorește afișarea doar a studenților cu un anumit nume (FirstName), o posibilǎ modificare a metodei Index din cadrul controller-ului Student ar fi urmǎtoarea:
public ActionResult Index(string nume)
{
var students = from s in db.Students select s;
if (!String.IsNullOrEmpty(nume))
{
students = students.Where(s => s.FirstName.Contains(nume));
}
return View(students);
}
Se utilizeazǎ o interogare LINQ pentru selectarea și returnarea doar a studenților care conțin în cadrul numelui șirul nume transmis ca parametru; dacǎ nu existǎ, se vor returna toți studenții. Chiar și fǎrǎ crearea unei vizualizǎri aferente, cǎutarea funcționeazǎ la nivel de server dacǎ se introduce, de exemplu, adresa: http://localhost:58269/Student?nume=Ovidiu .
Model binding
Acțiunile implementate în cadrul controller-ului pot sǎ specifice și parametri, aceștia fiind populați cu valori de cǎtre ASP.NET MVC utilizând informațiile din cerere atunci când aceasta se executǎ. Aceastǎ funcționalitate numitǎ model binding este una foarte puternicǎ și utilizatǎ în ASP.NET MVC.
Astfel, în lipsa facilitǎții model binding, regǎsirea unor parametri dintr-o cerere HTTP și transmiterea lor cǎtre o anumitǎ metodǎ-acțiune ar implica urmǎtorul cod:
public ActionResult Create()
{
var course = new Course()
{
CourseID = Request["courseID"],
Title = Request["title"],
Credits = Decimal.Parse (Request["credits"]),
};
// alte prelucrari
return View(course);
}
Codul implicǎ regǎsirea valorilor pentru parametrilor transmiși courseID, title și respectiv credits direct din obiectul dicționar Request; dacǎ parametri transmiși nu sunt de tipul string, în cadrul implementǎrii acțiunii trebuie realizatǎ și parsarea acestora. Construirea obiectului transmis pe baza parametrilor, maparea valorilor și, dacǎ este cazul, parsarea trebuie, în aceastǎ abordare, realizatǎ în mod explicit în corpul acțiunii.
ASP.NET MVC model binding simplificǎ codul acțiunilor controller-elor introducând un nivel de abstractizare suplimentar prin intermediul cǎruia se mapeazǎ și populeazǎ cu valorile transmise în cadrul cererii parametrii acțiunilor controller-elor. Astfel, în cazul utilizǎrii mecanismului de data-binding, toate aceste operațiuni se realizeazǎ în mod automat, fǎrǎ a fi necesarǎ utilizarea obiectului Request în mod explicit. Totuși, numele parametrilor este important deoarece aceștia trebuie sǎ corespundǎ valorilor din cerere. Astfel, codul metodei anterior prezentatǎ se simplificǎ:
public ActionResult Create(Course course)
{
// alte prelucrari care implica obietul course
//creat si populat cu valori din cerere
if (ModelState.IsValid)
{
db.Courses.Add(course);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(course);
}
La suprafațǎ model binding pare simplu dar de fapt, în spate existǎ un framework foarte complex. Mecanismul model-binding implicǎ regǎsirea parametrilor nu doar pe baza obiectului Request ci analizeazǎ și alte elemente cum ar fi obiecte JSON serializate, parametri unor interogǎri, valori transmise din cadrul form-urilor, etc.
Controller-ul Enrollments este construit pe același tipar ca și cele precedente. În plus, de exemplu acțiunea Index implicǎ afișarea tuturor cursurilor existente împreunǎ cu toți studenții înscriși la aceste cursuri utilizând db.Enrollments.Include și expresii lambda pentru a selecta studenții înscriși la fiecare curs. De asemenea, se observǎ utilizarea clasei SelectList din spațiul de nume System.Web.Mvc pentru crearea unei liste de selecție dropdown.
using MyUniversityApp.Models;
namespace MyUniversityApp.Controllers
{
public class EnrollmentController : Controller
{
private DBUniversityEntities db = new DBUniversityEntities();
//
// GET: /Enrollment/
public ActionResult Index()
{
var enrollments = db.Enrollments.Include(e => e.Course)
.Include(e => e.Student);
return View(enrollments.ToList());
}
//
// GET: /Enrollment/Details/5
public ActionResult Details(int id = 0)
{
Enrollment enrollment = db.Enrollments.Find(id);
if (enrollment == null)
{
return HttpNotFound();
}
return View(enrollment);
}
//
// GET: /Enrollment/Create
public ActionResult Create()
{
ViewBag.CourseID = new SelectList(db.Courses, "CourseID", "Title");
ViewBag.StudentID = new SelectList(db.Students, "StudentID", "LastName");
return View();
}
//
// POST: /Enrollment/Create
[HttpPost]
public ActionResult Create(Enrollment enrollment)
{
if (ModelState.IsValid)
{
db.Enrollments.Add(enrollment);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.CourseID = new SelectList(db.Courses, "CourseID", "Title",
enrollment.CourseID);
ViewBag.StudentID = new SelectList(db.Students, "StudentID", "LastName",
enrollment.StudentID);
return View(enrollment);
}
//
// GET: /Enrollment/Edit/5
public ActionResult Edit(int id = 0)
{
Enrollment enrollment = db.Enrollments.Find(id);
if (enrollment == null)
{
return HttpNotFound();
}
ViewBag.CourseID = new SelectList(db.Courses, "CourseID", "Title",
enrollment.CourseID);
ViewBag.StudentID = new SelectList(db.Students, "StudentID", "LastName",
enrollment.StudentID);
return View(enrollment);
}
//
// POST: /Enrollment/Edit/5
[HttpPost]
public ActionResult Edit(Enrollment enrollment)
{
if (ModelState.IsValid)
{
db.Entry(enrollment).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.CourseID = new SelectList(db.Courses, "CourseID", "Title",
enrollment.CourseID);
ViewBag.StudentID = new SelectList(db.Students, "StudentID", "LastName",
enrollment.StudentID);
return View(enrollment);
}
//
// GET: /Enrollment/Delete/5
public ActionResult Delete(int id = 0)
{
Enrollment enrollment = db.Enrollments.Find(id);
if (enrollment == null)
{
return HttpNotFound();
}
return View(enrollment);
}
//
// POST: /Enrollment/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
Enrollment enrollment = db.Enrollments.Find(id);
db.Enrollments.Remove(enrollment);
db.SaveChanges();
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
Filtre de acțiune în ASP.NET MVC
Filtrele de acțiune reprezintǎ o tehnicǎ foarte puternicǎ prin care se poate crea cod care sǎ se aplice diverselor componente ale aplicației (acțiuni ale controller-elor), indiferent care este principala responsabilitate a componentei respective. Logica unui filtru de acțiune este adǎugatǎ aplicației prin aplicarea unui atribut de tipul ActionFilterAttribute (clasǎ de bazǎ pentru toate tipurile de filtre) unui întreg controller sau doar unei acțiuni din cadrul unui controller. Prin intermediul atributului se controleazǎ (filtreazǎ) modul în care acțiunea respectivǎ este executatǎ.
La modul general, existǎ 4 categorii de filtre în ASP.NET MVC, și anume:
Filtre de autorizare – authorization filters – implementeazǎ interfața IAuthorizationFilter și sunt utilizate pentru a realiza decizii de securitate vizavi de execuția metodei acțiune: realizarea de autentificǎrii/autorizǎrii sau realizarea de validǎri ale unor proprietǎți ale cererii. Clasele AuthorizeAttribute (permite restricționarea accesului cǎtre un rol utilizator particular) și RequireHttpsAttribute sunt exemple de filtre de autorizare. Filtrele de autorizare se executǎ înaintea altor filtre.
Filtre de acțiune – action filters – implementeazǎ IActionFilter care expune douǎ metode: OnActionExecuting care se executǎ înaintea execuției metodei acțiune și OnActionExecuted care se executǎ dupǎ execuția metodei acțiune și poate realiza procesǎri adiționale. Sunt utilizate pentru a realiza procesǎri adiționale înaintea și dupǎ execuția metodei acțiune
Filtre de rezultat – result filters – implementeazǎ IResultFilter și acționeazǎ ca un wrapper pentru obiectul ActionResult. IResultFilter expune douǎ metode: OnResultExecuting se executǎ înainte ca obiectul ActionResult sa fie executat (și se creazǎ o vedere rezultat) iar OnResultExecuted dupǎ, putând realiza procesǎri adiționale asupra rezultatului, ca de exemplu modificarea vederii înainte ca aceasta sǎ fie randatǎ în browser. Clasa OutputCacheAttribute este un exemplu de fltru de rezultat – pǎstreazǎ în cache ieșirea unei acțiuni ale unui controller pentru o perioadǎ specificatǎ de timp
Filtre de excepție – exception filters – implementeazǎ IExceptionFilter și se executǎ dacǎ o excepție netratatǎ apare în timpul execuției. Se pot utiliza pentru a realiza operații precum creare de log-uri sau afișarea unei pagini de eroare. Clasa HandleErrorAttribute este un exemplu de filtru de excepție – trateazǎ erorile generate atunci când acțiunea se executǎ
Filtrele se executǎ în ordinea în care au fost descrise mai sus. Se pot, de asemenea, crea propriile filtre de acțiune. Un exemplu de utilizare a filtrelor îl gǎsim în controller-ul generat la crearea aplicației – AccountController.
Autorizare și autentificare
ASP.NET furnizeazǎ mai multe modalitǎți de autentificare dintre care cea mai cunoscutǎ este autentificarea pe bazǎ de formular. Aceasta implicǎ utilizarea unei pagini HTML în care utilizatorul introduce numele și parola care sunt transmise serverului pentru validare. Autentificarea implicǎ și existența unui mecanism de gestionare a utilizatorilor (membership API ) responsabil pentru gestionarea parolelor, a datelor aferente utilizatorilor, etc. Dacǎ nu se specificǎ altfel, ASP.NET utilizeazǎ în mod implicit o bazǎ de date predefinitǎ SQL Server ExpressDB pentru memorarea informațiilor aferente utilizatorilor.
Astfel, dacǎ se utilizeazǎ template-ul Internet Application în Visual Studio, se creazǎ controller-ul AccountController care furnizeazǎ o implementare de bazǎ a elementelor ce țin de autorizare și autentificare din cadrul aplicației. Atributul AuthorizeAttribute, care poate fi aplicat cǎtre acțiuni individuale ale controler-ului sau asupra întregului Controller și restricționeazǎ accesul numai cǎtre utilizatorii autentificați sau cǎtre anumiți utilizatori/roluri. Aplicarea acestui atribut întregului controller are ca și rezultat rejectarea oricǎrei cereri primitǎ de la utilizatori neautentificați. Totuși, pentru a permite anumite acțiuni și pentru utilizatorii anonimi, se poate aplica, în mod specific, atributul AllowAnonymous care permite accesul utilizatorilor neînregistrați.
În cazul în care se dorește securizarea întregii aplicații se pot utiliza filtre globale prin modificarea metodei RegisterGlobalFilters din Global.asax și adǎugarea atributului AuthorizeAttribute pentru întreaga aplicație.
Astfel, structura clasei AccountController generatǎ din Visual Studio este urmǎtoarea:
using MyUniversityApp.Filters;
using MyUniversityApp.Models;
namespace MyUniversityApp.Controllers
{
[Authorize]
[InitializeSimpleMembership]
public class AccountController : Controller
{
//
// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid && WebSecurity.Login(model.UserName,
model.Password, persistCookie: model.RememberMe))
{
return RedirectToLocal(returnUrl);
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "The user name or password provided
is incorrect.");
return View(model);
}
//
// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
WebSecurity.Logout();
return RedirectToAction("Index", "Home");
}
//
// GET: /Account/Register
[AllowAnonymous]
public ActionResult Register()
{
return View();
}
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
// Attempt to register the user
try
{
WebSecurity.CreateUserAndAccount(model.UserName, model.Password);
WebSecurity.Login(model.UserName, model.Password);
return RedirectToAction("Index", "Home");
}
catch (MembershipCreateUserException e)
{
ModelState.AddModelError("", ErrorCodeToString(e.StatusCode));
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
// ….alte metode actiune…..
}
AccountsController furnizeazǎ funcționalitǎți pre-implementate pentru operațiile tipice de gestionare utilizatorilor din cadrul aplicației: login, logoff, înregistrare de noi utilizatori, schimbare de parolǎ, etc., împreunǎ cu vederile asociate acestora care sunt de asemenea generate implicit.
Se observǎ utilizarea frecventǎ a decorǎrii metodelor acțiune (Login, Register) cu atributul AllowAnonymous. Acest lucru este datorat faptului cǎ a fost aplicat atributul AuthorizeAttribute la nivelul întregului controller și se dorește anularea acestuia pentru anumite metode specifice. De asemenea, la nivel global a mai fost aplicat atributul InitializeSimpleMembership prin care se asigurǎ faptul cǎ, înaintea realizǎrii oricǎror operații legate de utilizatori (login, register) , baza de date cu utilizatorii a fost creatǎ. Dacǎ nu, se creazǎ (localDB).
Un alt atribut prezent în mod frecvent și aplicat metodelor acțiune corespunzǎtoare cererilor de tip POST este AntiForgeryToken. Acesta este utilizat pentru a preveni atacuri de tipul cross-site scripting de tipul Cross Site Request Forgery (CSRF) care permit transmiterea unui formular de pe un alt domeniu și, în general, cu alte date decât cele dorite a fi introduse de cǎtre utilizator. ASP.NET MVC furnizeazǎ tehnici pre-implementate care pot fi utilizate în cadrul aplicației pentru a preveni acest tip de atac prin utilizarea atributului ValidateAntiForgeryToken asupra metodelor acțiune ale controller-ului în tandem cu helper-ul HTML AntiForgeryToken() în cadrul vederii asociate. De exemplu, pentru Login, vederea asociatǎ include urmǎtoarea secvențǎ:
@using (Html.BeginForm(new { ReturnUrl = ViewBag.ReturnUrl })) {
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
, care conține Html.AntiForgeryToken()în form-ul Html.BeginForm din View.
Atunci când se transmite form-ul, datele sunt transmise cǎtre metoda acțiune din cadrul controller-ului Login. Dacǎ metoda este decoratǎ cu atributul ValidateAntiForgeryToken, se valideazǎ datele pe baza unui token creat într-un cookie adițional creat atunci când se genereazǎ pagina. Token-ul din cookie și din form-ul transmis trabuie sǎ fie identic, în caz contrar se detecteazǎ un atac de tipul CSRF.
De exemplu token-ul generat în aplicația MyUniversityApp pentru secvența de logare este urmǎtorul (token-ul din cadrul form-ului, în variantǎ criptatǎ):
<input name="__RequestVerificationToken" type="hidden" value="pvbKZ2bVdDs36Lx81ejUSZIouqKIM5GBU4r_MJ1KR4taD5Ze7blML04j8nYSGgsBlZHhKMfoxVRk3DDcxhyX-ghKdeX-F13uD1h2mv5GQrE1" />
Se observǎ cǎ elementul <input> are un camp ascuns cu numele _RequestVerificationToken. Existǎ de asemenea creat un cookie cu același nume; când atributul ValidateAntiForgeryToken este setat în cadrul unui controller, se vor compara cookie-ul din cerere cu câmpul ascuns din cadrul form-ului. Token-ul se genereazǎ o datǎ pentru o sesiune.
Figura 11: Cookie-ul creat la generarea paginii
Dacǎ nu dorim sǎ ne implementǎm propriul sistem de înregistrare a utilizatorilor, putem sǎ permitem acestora sǎ se autentifice utilizând contul lor (existent!) de Facebook sau Google. Activarea opțiunii dorite se realizeazǎ prin configurǎrile realizate la nivelul aplicației. În directorul App_Start se identificǎ fișierul care conține informațiile de configurare pentru autentificarea utilizatorilor (AuthConfig.cs) și se elimina comentariile pentru opțiunea doritǎ din cadrul acestuia:
public static class AuthConfig
{
public static void RegisterAuth()
{
// To let users of this site log in using their accounts from other sites such as Microsoft, Facebook, and Twitter,
// you must update this site. For more information visit http://go.microsoft.com/fwlink/?LinkID=252166
//OAuthWebSecurity.RegisterMicrosoftClient(
// clientId: "",
// clientSecret: "");
//OAuthWebSecurity.RegisterTwitterClient(
// consumerKey: "",
// consumerSecret: "");
//OAuthWebSecurity.RegisterFacebookClient(
// appId: "",
// appSecret: "");
//OAuthWebSecurity.RegisterGoogleClient();
}
}
II.3. Nivelul Vizualizare (View) – Clasele Views
Toate vederile aplicației sunt localizate în directorul Views a aplicației. ASP.NET MVC localizeazǎ fiecare vedere (view) într-un director denumit dupǎ controller-ul care îi este asociat. Vederile au extensia .cshtml (pentru C#) sau .vbhtml (pentru Visual Basic). De exemplu, acțiunea About a controller-ului HomeController are asociatǎ vederea About.cshtml din subdirectorul Home:
Figura 12: Directorul Views
În mod asemǎnǎtor claselor Controller, Visual Studio asistǎ crearea de noi vederi (views); crearea unei vederi corespunzǎtoare unei anumite acțiuni a unui controller se poate face cu click dreapta pe metoda acțiune a controller-ului respectiv și Add View. Vederile sunt generate utilizând motoare de afișaj (view engines). ASP.NET MVC propune douǎ variante în acest sens: ASPX, disponibil încǎ de la prima lansare a MVC și Razor (preferat, introdus o datǎ cu MVC3). Dacǎ se dorește crearea unei vederi puternic tipizate trebuie selectat și tipul modelului; tipurile disponibile ar trebui sǎ aparǎ în listǎ dacǎ aplicația s-a compilat cu succes. Se pot crea și vederi parțiale.
Dacǎ nu se gǎsește o potrivire cu vederea care trebuie returnatǎ, ASP.NET MVC cautǎ în directorul Views/Shared. Conținutul fișierelor care implementeazǎ vederile este o combinație de HTML și cod C#; pentru realizarea imixtiunii dintre HTML și cod se utilizeazǎ sintaxa Razor.
Sintaxa Razor
Razor este o sintaxǎ care permite combinarea de cod și conținut HTML într-o manierǎ fluentǎ și expresivǎ. Nu este un nou limbaj, se bazeazǎ pe C# sau Visual Basic .NET pentru partea de cod. Diferǎ de alte sintaxe de marcare (de exemplu cea pentru web forms) fiind mult mai expresivǎ și permițând o trecere mult mai simplǎ între cod și conținut HTML.
Sintaxa Razor permite diferențierea dintre cod și marcaj HTML pe baza a douǎ elemente: code nuggets și blocuri de cod. De exemplu, conținutul fișierului About.cshtml este urmǎtorul:
@{
ViewBag.Title = "About";
}
<hgroup class="title">
<h1>@ViewBag.Title.</h1>
<h2>@ViewBag.Message</h2>
</hgroup>
<article>
<p>
MyUniversityApp is an application used for monitoring student enrollments to courses into a university.
</p>
</article>
<aside>
<h3>Other info</h3>
<p>
Use this links for other informations:
</p>
<ul>
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
</ul>
</aside>
Code nuggets – sunt expresii simple care sunt evaluate și randate inline. Încep imediat dupǎ simbolul @ și Razor idenificǎ care parantezǎ închisǎ indicǎ sfârșitul expresiei. Expresia întotdeauna returneazǎ HTML care va fi randat în view.
@Html.ActionLink("Home", "Index", "Home")
Blocurile de cod – reprezintǎ o parte (secțiune) a unui view și conțin doar cod (C#); trebuie sǎ respecte toate regulile de sintaxǎ aferente codului respectiv. Sunt delimitate de caracterele @{…}.
@{
ViewBag.Title = "About";
}
Spre deosebire de code nuggets, blocurile de cod nu randeazǎ nimic în view: permit scrierea de cod care nu returneazǎ niciun fel de valoare. Toate variabilele definite în cadrul blocurilor de cod pot fi utilizate în code nuggets din același domeniu de valabilitate.
Sintaxa Razor oferǎ de asemenea posibilitatea de a pǎtra un aspect al paginii consistent într-o aplicație web pe baza layout-urilor. Layout-urile acționeazǎ ca un template general la care toate vederile (views) aderǎ, definind stilul paginilor aplicației într-o manierǎ uniformǎ și conlucrând la uniformizarea aspectului. Crearea layout-urilor se bazeazǎ conbinația
HTML + CSS + script-uri JavaScript. Fișierul de layout implicit creat se gǎsește în Views/Shared și este _Layout.cshtml. În cazul aplicației MyUniversity acesta aratǎ astfel:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>@ViewBag.Title – My ASP.NET MVC Application</title>
<link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<meta name="viewport" content="width=device-width" />
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
</head>
<body>
<header>
<div class="content-wrapper">
<div class="float-left">
<p class="site-title">@Html.ActionLink("MyUniversity –
ASP.NET MVC", "Index", "Home")</p>
</div>
<div class="float-right">
<section id="login">
@Html.Partial("_LoginPartial")
</section>
<nav>
<ul id="menu">
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Students", "Index", "Student")</li>
<li>@Html.ActionLink("Courses", "Index", "Course")</li>
<li>@Html.ActionLink("Enrollments", "Index",
"Enrollment")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
</ul>
</nav>
</div>
</div>
</header>
<div id="body">
@RenderSection("featured", required: false)
<section class="content-wrapper main-content clear-fix">
@RenderBody()
</section>
</div>
<footer>
<div class="content-wrapper">
<div class="float-left">
<p>© @DateTime.Now.Year – MyUniversity Application</p>
</div>
</div>
</footer>
@Scripts.Render("~/bundles/jquery")
@RenderSection("scripts", required: false)
</body>
</html>
Fișierul se bazeazǎ pe diferite variabile ca de exemplu ViewBag.Title precum și pe funcțiile helper @RenderSection și @RenderBody() pentru interacțiunea cu vederi individuale: anumite porțiuni individuale specifice sunt randate în poziția în care apar aceste apeluri. Metoda RenderBody poate apǎrea o singurǎ datǎ în declarația unui layout însǎ RenderSection se poate utiliza pentru a indica amplasamentul diferitelor secțiuni în cadrul paginii. Parametrul required al metodei indicǎ faptul cǎ secțiune este obligatorie (true) sau opționalǎ (false). În cazul valorii true, apare o eroare la execuție dacǎ se întâlnește o vizualizare care nu o conține.
În general, arhitectura MVC care impune separarea dintre controller-e și vederi trebuie totuși sǎ punǎ la dispoziție un mecanism prin care sǎ se transmitǎ date între cele douǎ niveluri controller-view. Acest lucru se realizeazǎ practic utilizând ViewData și/sau TempData: obiecte dicționar disponibile ca și proprietǎți atât în obiectele controller cât și în obiectele View. Prin intermediul acestora se pot transmite date, sub formǎ de perechi nume-valoare.
În practicǎ însǎ, în loc de proprietatea ViewData se utilizeazǎ proprietatea similarǎ ViewBag. ViewBag este un wrapper în jurul proprietǎții ViewData care expune proprietatea dicționar ViewData ca și un obiect dinamic. Deci, în loc de a referi datele ca și cum ar fi conținute într-un dicționar: >@ViewData["Title"], se pot referi ca fiind proprietǎți ale unui obiect prin: ViewBag.Title.
ViewData mai conține de asemenea o proprietate importantǎ: proprietatea Model – care reprezintǎ obiectul model care reprezintǎ ținta cererii. În mod implicit însǎ proprietatea Model disponibilǎ utilizând Razor este dinamicǎ – poate fi accesatǎ fǎrǎ a-i cunoaște exact tipul. Totuși, dat fiind natura staticǎ a limbajului C#, este necesar sǎ se precizeze tipul modelului la care se referǎ prezentarea în mod explicit utilizând în Razor utilizând cuvântul cheie @model, de preferat la începutul fișierului. De exemplu, vederea Details.cshtml definește ca și model clasa Course cǎreia mai apoi îi acceseazǎ proprietǎțile model.Title și model.Credits:
@model MyUniversityApp.Models.Course
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<fieldset>
<legend>Course</legend>
<div class="display-label">
@Html.DisplayNameFor(model => model.Title)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Title)
</div>
<div class="display-label">
@Html.DisplayNameFor(model => model.Credits)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Credits)
</div>
</fieldset>
<p>
@Html.ActionLink("Edit", "Edit", new { id=Model.CourseID }) |
@Html.ActionLink("Back to List", "Index")
</p>
Prima linie definește tipul modelului astfel încât orice referire la acesta fiind puternic tipizatǎ și direct accesibilǎ.
În afara sintaxei Razor, ASP.NET MVC oferǎ o serie de așa numite „ajutoare” – helper-i destinați generǎrii mai simple de cod HTML. Cei mai utilizați helper-i sunt cei reprezentați de clasele HtmlHelper și UrlHelper expuși în controller-e și în vederi ca proprietǎțile Html și Url și care furnizeazǎ diferite metode suport care pot fi utilizate. De exemplu, utilizând metoda-suport @Html.ActionLink secvența:
@Html.ActionLink("Students", "Index", "Student")
, genereazǎ urmǎtorul conținut HTML:
<a href="/Student">Students</a>
Alte metode suport ca de exemplu @Html.LabelFor(…) sau @Html.DisplayNameFor permit realizarea de apeluri la controalele HTML corespunzǎtoare într-o manierǎ foarte simplǎ și intuitivǎ utilizând expresii lambda.
De exemplu, dacǎ se dorește completarea vizualizǎrii la nivelul fișierului Index.cshtml pentru a include facilitatea de selectare a studenților dupǎ nume (FirstName) se poate utiliza @Html.TextBox pentru introducerea parametrului nume utilizat în procesul de selecție:
@using (Html.BeginForm())
{
<p>
Nume: @Html.TextBox("nume")
<input type="submit" value="Cauta" />
</p>
}
Se observǎ utilizarea Html.BeginForm care cauzeazǎ realizarea unui POST cǎtre aceeași paginǎ pentru ca datele introduse în câmpul @Html.TextBox sǎ populeze parametrul nume dupǎ care se realizeazǎ cǎutarea. Alternativa ar fi fost realizarea unei metode index cu atributul [HttpPost] dar care nu e absolut necesarǎ deoarece nu se modificǎ datele aplicației ci doar se filtreazǎ dupǎ anumite criterii.
II.4. Nivelul Model – Clasele Model
Nivelul model constǎ în mod normal din clase obișnuite care expun datele aplicației sub formǎ de proprietǎți împreunǎ cu logica aferentǎ acestora reprezentatǎ de metode. Aceste clase pot sǎ imbrace diferite forme: modelul de date sau modelul domeniului. Modelul poate sǎ defineascǎ diferite funcționalitǎți; unul dintre elementele importante ale modelului îl reprezintǎ validarea datelor.
Deoarece framework-ul ASP.NET MVC se dorește a fi extrem de flexibil în ceea ce privește accesul la date, este posibilǎ utilizarea diferitelor componente/framework-uri pentru acces la date: ADO.NET, LINQ to SQL, ADO.NET Entity Framework sau NHibernate.
utilizarea ADO.NET implicǎ gestionarea directǎ (scrirea și executarea) a interogǎrilor SQL
recomandarea alternativǎ este aceea de a utiliza un ORM (Object Relational Mapper). Cele mai uzuale ORM-uri care pot fi utilizate cu ASP.NET sunt ADO.NET Entity Framework, LINQ to SQL și NHibernate. ADO.NET Entity Framework este cel mai utilizat ORM furnizat de MicroSoft care, începând cu versiuna 6.0 este open source.
Implementarea efectivǎ a accesului la date se bazeazǎ mai multe șabloane; cele mai cunoscute sunt Object relational mapper (ORM) și respectiv Repository.
II.4.1. Șablonul object relational mapper (ORM)
Este un șablon care permite adresarea, accesul și manipularea obiectelor unei aplicații fǎrǎ a ține cont de modalitatea în care acestea sunt legate de anumite surse de date specifice. Permite menținere unei abordǎri consistente a obiectelor prin prisma entitǎților domeniului aplicației.
Bazat pe diverse abstractizǎri, șablonul ORM managerizeazǎ aspectele concrete legate de acces la date și prezintǎ entitǎți pentru maparea dintre clasele (tipurile .NET) din modelul obiectual al domeniului și elementele (tabele, relații) din modelul relațional de date, în același timp ascunzând detaliile realizǎrii acestor operații fațǎ de codul aplicației.
Este un șablon aferent modelului domeniului – obiectual, care modeleazǎ entitǎțile din lumea realǎ în special din punctul de vedere al comportamentului acestora. Modelul domeniului nu depinde decât de cerințele aplicației, nedepinzând de nimic altceva adicǎ este cu desǎvârșire ignorant vizavi de modalitatea în care se asigurǎ persistența.
Modelul de persistențǎ sau de date – relațional – modeleazǎ cum sunt stocate datele din punctul de vedere al structurii de stocare – se bazeazǎ pe modelul domeniului și depinde de constrângerile necesare asigurǎrii persistenței.
Cele douǎ modele servesc scopuri complet diferite și au diferite responsabilitǎți, chiar dacǎ, în situații de aplicații foarte simple acestea ar putea sǎ concidǎ. De cele mai multe ori însǎ existǎ diferențe semnificative de structurǎ între modelul domeniului (obiectual) și cel al persistenței (relațional), așa numitul object-relational impedance mismatch. Acest lucru se datoreazǎ faptului cǎ cea mai optimǎ structurǎ din modelul obiectual al domeniului cu cea din modelul relațional al persistenței nu se potrivesc:
uneori modelul va conține mai multe clase decât tabele in baza de date
moștenirea – concept cheie în programarea obiectualǎ nu se regǎsește ca tip de relație în modelul relațional al unei baze de date – va trebui reprezentatǎ altfel
asociațiile dintre clase – .NET Framework reprezintǎ legǎturile de asociere dintre obiecte ca referințe unidirecționale dar într-o bazǎ de date, o legǎturǎ între tabele poate fi utilizatǎ în ambele direcții. De asemenea, e imposibil de cunoscut multiplicitatea asocierilor dintr-o clasǎ: conceptul one-to-many sau many-to-many nu pot fi diferențiate
navigarea prin datele modelului obiectual .NET este fundamental diferitǎ de cea realizatǎ prin tabelele bazei de date, de exemplu, în cazul unei interogǎri
Astfel, existența unui ORM rezolvǎ toate aceste neconcordanțe între cele doua modele, domeniu- obiectual și persistențǎ-relațional creând un nivel de abstractizare suplimentar între acestea. Șablonul ORM îmbracǎ mai multe forme care pot fi utilizate cu ASP.NET MVC și anume LINQ to SQL și ADO.NET Entity Framework de la MicroSoft și respectiv nHibernate. Dintre acestea, cel mai complex și cu funcționalitǎți multiple este Entity Framework.
Entity Framework EF
Este un ORM (Object Relational Mapper) inclus în .NET Framework. Utilizând EF, dezvoltatorul interacționeazǎ doar cu modelul entitate al domeniului – obiectual în loc de modelul de date – relațional. Abstracțiile modelului permit focalizarea cǎtre comportamentul și relațiile dintre entitǎți (obiecte) ignorând detaliile de stocare aferente modelului relațional. Efortul de dezvoltare se concentreazǎ la nivel conceptual asupra entitǎților domeniului iar efortul de a asigura și gestiona persistența este lǎsat pe seama Entity Framework.
Figura 12: Arhitectura Entity Framework
Arhitectura Entity Framework implicǎ nivelurile prezentate în Figura 12.
Entity Data Model (EDM) reprezintă modelul conceptual al datelor, folosind o metodă de modelare numită la fel, Entity Data Model, o versiune extinsă a Entity Relationship model. Modelul de date descrie entitățile și asociațiile dintre ele.
Schema EDM este exprimată ȋn SDL (Schema Definition Language) care este bazatǎ pe XML. De asemenea maparea elementelor schemei conceptuale ȋn schema de stocare fizicǎ este de asemenea bazată pe XML. ADO.NET Entity Framework folosește EDM pentru a realiza maparea permițând aplicației să lucreze cu entități, ȋn timp ce intern se abstractizează utilizarea unor construcții ADO.NET precum DataSet și RecordSet.
Visual Studio oferǎ un designer de entități pentru crearea vizuală a EDM-ului și specificațiile de mapare pentru acesta. Rezultatul acestui instrument este un fișier XML cu extensia .edmx reprezentând schema și specificațiile de mapare. Fișierul .edmx include conținut metadata CSDL/MSL/SSDL care poate fi creat și editat manual. Un model .edmx creat utilizând Entity Framework este prezentat în Figura 5.
ADO.NET Entity Framework folosește un limbaj SQL numit Entity SQL care vizează scrierea de interogări declarative asupra entităților și legaturilor dintre entități la un nivel conceptual. Diferă de SQL prin faptul că nu dispune de construcții explicite pentru join-uri deoarece EDM este conceput să abstractizeze partiționarea datelor peste tabele. Interogarea modelului conceptual este facilitată de clasele client EntityClient, care acceptă o interogare de tip Entity SQL.
Existǎ mai multe abordǎri pentru accesul la date atunci când se utilizeazǎ Entity Framework, funcție de unde se pornește în dezvoltarea aplicației: Database First, Model First sau Code First. Indiferent de abordarea aleasǎ, Entity Framework materializeazǎ datele returnate din baza de date în obiecte entitate și propagǎ schimbǎrile din cadrul obiectelor cǎtre baza de date. Clasa responsabilǎ pentru interacțiunea cu obiectele este clasa context: System.Data.Entity.DbContext. Ea expune colecțiile de obiecte ca și proprietǎți de tipul DbSet. Dacǎ se utilizeazǎ Entity Framework Designer, atunci contextul este generat automat; dacǎ nu, trebuie creat manual.
a. DataBase First
Aceastǎ abordare este potrivitǎ pentru dezvoltarea aplicațiilor centrate pe date care presupun existența inițialǎ a bazei de date și generarea, pe baza acesteia, a modelului domeniului. Pe baza unei conexiuni, de exemplu, cǎtre o bazǎ de date SQL Server, utilizând wizard-ul specific din Visual Studio (în directorul Models: Add/New Item/Data/ADO.NET Entity Data Model – Generate from DataBase) se scaneazǎ structura bazei de date existente și se construiește modelul care poate fi vizualizat în Entity Framework. În decursul creǎrii, se poate selecta intregral structura bazei de date sau doar o parte din aceasta. Modelul poate fi regenerat atunci când apar modificǎri de structurǎ la nivelul bazei de date sau modificat oricând se doreșțe utilizând designer-ul din Entity Framework. Modelul din Figura 5 a fost generat în acest mod.
Figura 13: Fișierele model generate de Entity Framework
Fișierul MyUniversity.Context.cs care se genereazǎ reprezintǎ așa numitul context al modelului și conține o clasǎ derivatǎ din DbContext, care include proprietǎți aferente fiecǎrei clase model care corespunde unui table din baza de date, de tipul DbSet. O înregistrare din tabel corespunde unui obiect din cadrul setului de obiecte. Toate controller-ele aplicației vor utiliza în implementare o instanțǎ a clasei context DBUniversityEntities astfel create.
namespace MyUniversityApp.Models
{
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
public partial class DBUniversityEntities : DbContext
{
public DBUniversityEntities()
: base("name=DBUniversityEntities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
}
}
Clasele Course.cs, Enrollment.cs, and Student.cs sunt clasele model care reprezintǎ tabelele din baza de date. Se observǎ modul în care sunt modelate relațiile de tipul on-to-many între clasele Course-Enrollment și respectiv Student-Enrollment utilizând proprietǎțile de navigare Enrolments din cadrul claselor Course și Student:
o relație one-to-many dintre Student și Enrollment – un student poate fi înscris la mai multe cursuri
o relație one-to-many dintre Course și Enrollment – un curs poate avea mai multi studenți înscriși
Aceste proprietǎți corespund foreign-key-urilor din baza de date.
namespace MyUniversityApp.Models
{
using System;
using System.Collections.Generic;
public partial class Student
{
public Student()
{
this.Enrollments = new HashSet<Enrollment>();
}
public int StudentID { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public Nullable<System.DateTime> EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
public partial class Enrollment
{
public int EnrollmentID { get; set; }
public Nullable<decimal> Grade { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
public virtual Course Course { get; set; }
public virtual Student Student { get; set; }
}
public Course()
{
this.Enrollments = new HashSet<Enrollment>();
}
public int CourseID { get; set; }
public string Title { get; set; }
public Nullable<int> Credits { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
b. Model First
Atunci când nu existǎ o bazǎ de date, Entity Framework oferǎ posibilitatea de a crea modelul de date conceptual direct în designer-ul framework-ului. La fel ca și în cazul Database First, se utilizeazǎ un fișier pentru memorarea schemei bazei de date și apoi, care va fi utilizat pentru a se genera efectiv baza de date. Crearea modelului implicǎ definirea entitǎtilor abstracte și maparea acestora cǎtre tabelele din baza de date, crearea relațiilor între acestea, etc. Practic, opțiunea e utilǎ atunci când se preferǎ crearea schemei relaționale în EF și nu în mod clasic, de exemplu utilizând SQL Server Management Studio (SSMS). Abordarea e de fapt tot orientatǎ cǎtre date deoarece se ține cont de modelul de persistențǎ, doar cǎ abstractizeazǎ modalitatea de creare a structurii bazei. Aceastǎ variantǎ nu se bucurǎ de prea multǎ popularitate deoarece se focalizeazǎ tot pe date, ca și DataBase First introducând în plus un nivel de abstractizare suplimentar și implicit o complexitate mai mare în abordare.
c. Code First
Este varianta care permite o abordare care nu ține cont în niciun fel de modul în care va fi asiguratǎ persistența. Permite proiectarea modelului în termeni de entitǎți ale domeniului (obiecte descrise prin clase) și relații între ele, folosind direct cod și delegarea tuturor operațiilor legate de persistențǎ. În acest caz, EF nu se mai bazeazǎ pe niciul fel de fișier extern care sǎ conținǎ schema bazei de date, nu mai existǎ fișierul .edmx sau de mapare .xml, ci utilizeazǎ un API specific care genereazǎ schema bazei de date în mod dinamic, la execuție.
Proiectarea de tipul Code First utilizeazǎ la crearea modelului domeniului obiecte POCO. Un obiect POCO – Plain Old CLR Objects este o clasǎ .NET utilizatǎ pentru reprezentarea unei entitǎți a modelui domeniului, cu atribute și metode specifice logicii aplicației. Principalul scop al claselor de tip POCO îl reprezintǎ acela de a proiecta modelul domeniului aplicației fǎrǎ a cunoaște forma de persitențǎ a datelor (persistence ignorance) și astfel acesta sǎ poatǎ evolua independent de modelul de acces la date concret.
Utilizarea obiectelor POCO pentru modelarea entitǎților domeniului aplicației și prezintǎ o serie de avantaje:
oferǎ un mecanism simplu de stocare pentru date și simplificǎ procesul de serializare și tranmitere a datelor între nivelurile aplicației MVC
prin simplitatea lor, faciliteazǎ cuplajul redus între niveluri și implicit minimizeazǎ complexitatea dependențelor dintre acestea
Astfel, varianta Code-First reprezintǎ o abordare deosebit de flexibilǎ, orientatǎ spre cod care beneficiazǎ de faptul cǎ nu este legatǎ în niciun fel de o tehnologie de persistențǎ specificǎ. Pașii care trebuie urmați în implementarea unei asfel de abordǎri implicǎ, în principiu, etapele inverse fațǎ de opțiunea Database First. Astfel clasele model și contextul care în abordarea Database First se generau utilizând EF, în aceastǎ variantǎ trebuie create inițial asftel încât, pe baza acestora EF sǎ genereze la final baza de date.
Pentru o aplicație similarǎ cu MyUniversityApp astfel creatǎ, pașii ar fi urmǎtorii:
Se crează clasele entitate ale modelului din cadrul aplicației : Course, Enrollment, Student cu următoarele proprietăți de navigare :
relație one-to-many dintre Student și Enrollment – un student poate fi înscris la mai multe cursuri
relație one-to-many dintre Course și Enrollment – un curs poate avea mai multi studenți înscriși
Noua soluție se numește MvcCodeFirst, dar este în esența aceeiași aplicație de gestionare a studenților/cursurilor din varianta Database First. Clasele model create sunt urmǎtoarele:
namespace MvcCodeFirst.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
public class Student
{
public int StudentID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
public Grade? Grade { get; set; }
public virtual Course Course { get; set; }
public virtual Student Student { get; set; }
}
}
Trebuie, de asememea creat contextul bazei de date: de obicei acesta se crează într-un nou director din Solution Explorer dedicat acceului la date (DAL – Data Access Layer). Fișierul DbUniversity.cs include clasa derivată din DbContext:
using System.Collections.Generic;
using System.Linq;
using System.Web;
using MvcCodeFirst.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace MvcCodeFirst.DAL
{
public class DbUniversity : DbContext
{
public DbSet<Student> Students { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Course> Courses { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
}
}
In cadrul clasei context se creeazǎ de asemenea proprietățile DbSet asociate întregului set de date care, de obicei, corespund unui tabel din baza de date. O entitate corespunde unei înregistrări din tabel (Student, Enrollment, Course). De asemenea, prin convenție, numele tabelelor create în baza de date asociate seturilor de date sunt create prin pluralizarea numelor entităților singulare (Course – Courses, etc). Această convenție nu este însǎ obligatorie, se poate seta la creare dacǎ se dorește sau să se utilizeze plurarul pentru denumirea tabelelor (metoda Remove eliminǎ aceastǎ convenție)
Dacă nu se specifică altfel, Entity Framework asignează un nume identic cu cel al clasei pentru șirul de conectare la baza de date ; șirul de conectare trebuie adăugat în fișierul Web.config. De exemplu, dacă se utilizează LocalDB – o variantă light a SQL Server Express Database Engine, secvența de adǎugat ar putea fi urmǎtoarea:
<add name="DBUniversity" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=DBMyUniversity;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\DBMyUniversity.mdf" providerName="System.Data.SqlClient"/>
Atunci când se dezvoltǎ o aplicație modelul de date se schimbǎ frecvent și acest lucru implicǎ o sincronizare cu baza de date. Entity Framework poate fi configurat astfel încât schimbǎrile din model sǎ se reflecte în mod automat în baza de date fǎrǎ a trebui recreatǎ baza de date (pierzând eventualele date introduse în aceasta pânǎ la acel moment). Facilitatea de migrare (Migration) permite, în abordarea Code First, sǎ se modifice baza de date de câte ori modelul se schimbǎ fǎrǎ a pierde conținutul inițial. Pentru a putea utiliza migrarea, trebuie utilizatǎ comanda enable-migrations în Package Manager Console.
NuGet Package Manager
La crearea noilor proiecte în Visual Studio, fiecare template de proiect ales utilizeazǎ NuGet Package Manager pentru instalarea și managerizarea referințelor cǎtre alte ansamble din cadrul aplicației, pentru gestionarea dependențelor aplicației. NuGet pǎstreazǎ și gestioneazǎ relațiile complexe dintre ansamblele de care aplicația depinde (directorul References din Solution Explorer) versiunile acestora și, de asemenea, permite adǎugarea unor noi dependințe la proiect. NuGet nu este parte a ASP.NET Framework.
NuGet Package Manager are o interfațǎ graficǎ accesibilǎ din Solution Explorer prin selectarea opțiunii Manage NuGet Packages sau se poate accesa în mod consolǎ: Tools/Library Package Manager /Package Manager Console. Existǎ o serie de comenzi pe care acesta le cunoaște și pot fi utilizate în consecințǎ; pentru migrare, comenzile posibile ar fi urmǎtoarele:
enable-migrations – contexttypename DbUniversity – migrarea este activatǎ și se transmite numele clasei context care se va utiliza (DbUniversity)
add-migration InitialCreate – se creazǎ clasa InitialCreate derivatǎ din DbMigration
update-database – se creazǎ/actualizeazǎ baza de date rulându-se și metoda Seed prin care aceasta se poate inițiliza cu valori daca se dorește
Figura 14 : Lansare Package Manager Console
In urma acestor comenzi, se creazǎ un director Migrations, cu douǎ fișiere : 201408140948187_InitialCreate.cs și respectiv Configuration.cs.
Figura 15 : Directorul Migrations
Fișierul 201408140948187_InitialCreate.cs conține codul pentru crearea efectivǎ a bazei.
namespace MvcCodeFirst.Migrations
{
using System;
using System.Data.Entity.Migrations;
public partial class InitialCreate : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.Student",
c => new
{
StudentID = c.Int(nullable: false, identity: true),
LastName = c.String(),
FirstMidName = c.String(),
EnrollmentDate = c.DateTime(nullable: false),
})
.PrimaryKey(t => t.StudentID);
CreateTable(
"dbo.Enrollment",
c => new
{
EnrollmentID = c.Int(nullable: false, identity: true),
CourseID = c.Int(nullable: false),
StudentID = c.Int(nullable: false),
Grade = c.Int(),
})
.PrimaryKey(t => t.EnrollmentID)
.ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
.ForeignKey("dbo.Student", t => t.StudentID, cascadeDelete: true)
.Index(t => t.CourseID)
.Index(t => t.StudentID);
CreateTable(
"dbo.Course",
c => new
{
CourseID = c.Int(nullable: false),
Title = c.String(),
Credits = c.Int(nullable: false),
})
.PrimaryKey(t => t.CourseID);
}
public override void Down()
{
DropIndex("dbo.Enrollment", new[] { "StudentID" });
DropIndex("dbo.Enrollment", new[] { "CourseID" });
DropForeignKey("dbo.Enrollment", "StudentID", "dbo.Student");
DropForeignKey("dbo.Enrollment", "CourseID", "dbo.Course");
DropTable("dbo.Course");
DropTable("dbo.Enrollment");
DropTable("dbo.Student");
}
}
}
Dacă se dorește, se poate furniza și o implementare pentru metoda de inițializare cu date a bazei, pentru a include date de test – metoda Seed din fișierul Configuration.cs, creat în urmǎtoarea formǎ :
namespace MvcCodeFirst.Migrations
{
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
internal sealed class Configuration : DbMigrationsConfiguration<MvcCodeFirst.DAL.DbUniversity>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(MvcCodeFirst.DAL.DbUniversity context)
{
// This method will be called after migrating to the latest version.
// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
}
}c:\users\doina\documents\visual studio 2012\Projects\MvcCodeFirst\MvcCodeFirst\packages.config
}
Implementarea metodei trebuie sǎ urmǎreascǎ template-ul furnizat.
La rularea aplicației Entity Framework va crea în mod automat baza de date. Acest lucru se va observa imediat în Server Explorer :
Figura 16 : Generarea bazei de date
II.4.2. Validarea modelului
Indiferent de maniera de creare a modelului, un aspect important aferent acestuia îl constituie validarea datelor introduse în cadrul acestuia. Astfel, pentru realizarea validǎrilor în conformitate cu cerințele unei aplicații web, ASP.NET MVC se bazeazǎ și în acest caz pe o serie de convenții și reguli specifice în acest scop. Un element care joacǎ un rol important în procesul de validare este obiectul ModelState.
Se observǎ din implementǎrile claselor controller prezentate în capitolul II.2 Nivelul controller – Clasele Controller, faptul cǎ metodele Create/Edit utilizeazǎ obiectul ModelState pentru validarea datelor intoduse de utilizator prin: ModelState.IsValid.
Ca parte a procesǎrilor executate în cadrul acțiunilor controller-elor, ASP.NET MVC valideazǎ toate datele care sunt trimise cǎtre o acțiune a unui controller populând un obiect ModelState cu informații despre validǎri care au eșuat și transmițând acest obiect cǎtre controller. Pe lângǎ validǎrile implicit realizate de .NET Framework, utilizatorul poate, la rândul sǎu sǎ includǎ propriile erori de validare în ModelState utilizând ModelState.AddModelError. Controller-ul poate apoi decide dacǎ cererea este validǎ sau nu și reacționa în consecințǎ (de exemplu, solicitând utilizatorul sǎ se corecteze erorile de validare prin întoarcerea la form-ul de introducere de unde acestea au provenit).
De exemplu, dacǎ dorim restricționarea datei de înscriere introduse în cadrul controller-ului Student:
// GET: /Student/Create
public ActionResult Create()
{
return View();
}
//
// POST: /Student/Create
[HttpPost]
public ActionResult Create(Student student)
{
if (student.EnrollmentDate >= DateTime.Now)
{
ModelState.AddModelError(
"EnrollmentDate",
"Data inscrierii trebuie sa fie anterioara celei de azi!"
);
}
//aplicam un bloc try – daca nu se pot salva datele
try
{
if (ModelState.IsValid)
{
db.Students.Add(student);
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException)
{
//inregistreaza eroarea in ModelState
ModelState.AddModelError("", "Nu se pot salva modificarile!");
}
return View(student);
}
Aceastǎ abordare în ceea ce privește validarea modelului funcționeazǎ foarte bine dar are tendința de a nu fi în acord cu conceptul separation of concerns specific arhitecturii MVC: validarea datei de înscriere nu ar trebui sǎ aparǎ în controller, fiind specificǎ modelului:
// validare data inscriere
if (student.EnrollmentDate >= DateTime.Now)
{
// adaugare eroare în ModelState
ModelState.AddModelError(
"EnrollmentDate",
"Data inscrierii trebuie sa fie anterioara celei de azi!"
);
}
Mutarea acestor gen de validǎri din controller cǎtre model se poate realiza prin decorarea claselor aferente modelului cu atribute din spațiul de nume System.ComponentModel.DataAnnotations. Astfel, API-ul Data Annotations din .NET Framework (core) furnizeazǎ un set de atribute predefinite prin care se pot aplica reguli de validare într-o manierǎ declarativǎ, direct la nivel de model.
De exemplu, clasa model Student din cadrul aplicației MyUniversityApp poate utiliza diferite atribute de validare dupǎ cum urmeazǎ:
namespace MyUniversityApp.Models
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
public partial class Student
{
public Student()
{
this.Enrollments = new HashSet<Enrollment>();
}
public int StudentID { get; set; }
[Required, StringLength(50)]
public string LastName { get; set; }
[Required, StringLength(50)]
public string FirstName { get; set; }
[DataType(DataType.Date)]
public Nullable<System.DateTime> EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Se observǎ utilizarea atributelor [Required] [StringLength(50)] și respectiv [DataType]. Alte atribute des utilizate sunt [Range(x,x)] pentru valori numerice. De exemplu, clasa Enrollment:
namespace MyUniversityApp.Models
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
public partial class Enrollment
{
public int EnrollmentID { get; set; }
[Range(0, 4)]
public Nullable<decimal> Grade { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
public virtual Course Course { get; set; }
public virtual Student Student { get; set; }
}
}
Dacǎ dorim realizarea de validǎri mai complexe se poate utiliza atributul [RegularExpression] unde validarea poate sǎ aibe la bazǎ o expresie regulatǎ. În cadrul atributelor se pot, de asemenea, include și anumite mesaje de eroare mai specifice fațǎ de cele implicit generate de API-ul Data Annotation, de exemplu, pentru atributul Range:
[Range(0, 4, ErrorMessage = "Domeniu de valori doar intre 0 si 4!")]
O altǎ utilizare importantǎ a atributelor este legatǎ de numele câmpurilor care apar în formularul de editare. Este cunoscut faptul cǎ, etichetarea câmpurilor care sunt generate la adǎugare și editare (și apar în cadrul view-ului corespunzǎtor) se realizeazǎ implicit cu numele proprietǎții corepunzǎtoare din cadrul clasei. Aceastǎ abordare nu este întotdeauna cea mai sugesivǎ deoarece în acest caz nu putem avea spații în denumiri sau, eventual, alte nume mai sugestive; e recomandat sǎ se schimbe numele sub care apar câmpurile utilizând atributul Display:
[Display(Name=”Data inmatricularii”)]
public Nullable<System.DateTime> EnrollmentDate { get; set; }
Utilizarea atributelor de validare este simplǎ dar are un dezavantaj important: în cazul utilizǎrii abordǎrii DataBase First pentru generarea modelului, în cadrul cǎreia se creazǎ modelul dupǎ baza de date, atributele se pierd dacǎ se schimbǎ baza de date și se regenereazǎ modelul. Pentru a evita aceastǎ problemǎ se pot utiliza metadate. În aceastǎ abordare se creazǎ clase separate, parțiale, care conțin doar acele proprietǎți din cadrul modelului pentru care se dorește aplicarea unor atribute de validare. Se exemplu, pentru clasele Student și Enrollment, acestea ar fi urmǎtoarele:
//clase pentru atribute de validare
public class StudentMetadata
{
[StringLength(50)]
public string LastName;
[StringLength(50)]
public string FirstName;
}
public class EnrollmentMetadata
{
[Range(0, 4)]
public Nullable<decimal> Grade;
}
De asemenea, în directorul Models al aplicației se creazǎ clase parțiale identice cu cele din model asupra cǎrora li se aplicǎ metadatele definite anterior:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
namespace MyUniversityApp.Models
{
//clase partiale cu atributul pentru aplicare metadate de validare
[MetadataType(typeof(StudentMetadata))]
public partial class Student
{
}
[MetadataType(typeof(EnrollmentMetadata))]
public partial class Enrollment
{
}
}
Într-o astfel de implementare atributele nu dispar chiar dacǎ modelul este regenerat.
Existǎ de asemenea posibilitatea ca utilizatorul sǎ-și poatǎ defini propriile atribute de validare în cazul în care cele predefinite nu satisfac cerințele, sau, în cazul în care s-ar dori realizarea unor validǎri combinate, mai complexe. Clasa ValidationAttribute se utilizeazǎ în acest scop: practic, se suprascrie metoda isValid pentru instanța clasei cǎreia îi aplicǎm validarea. De exemplu, dacǎ dorim crearea unui atribut CreditePare pentru a valida câmpul Credits din cadrul clasei Course și a permite doar introducerea de credite pare în domeniul de valori dat, implementarea unei clase aferente atributului CreditePare ar fi urmǎtoarea:
public class CreditePare:ValidationAttribute
{ //doar valori pentru credite 2,4 si 6!
protected override ValidationResult IsValid
(object value, ValidationContext validationContext)
{
//obtinem o instanta => obiect de tipul Course
var model = (Course)validationContext.ObjectInstance;
if (model.Credits != 2 && model.Credits != 4 && model.Credits != 6)
return new ValidationResult("Creditele pot fi numai 2, 4 sau 6!");
return ValidationResult.Success;
}
}
Aplicarea atributului se poate realiza similar cu cele predefinite; dacǎ utilizǎm opțiunea care se bazeazǎ pe matadate, se poate completa clasa cu:
public class CreditsMetadata
{
[CreditePare]
public Nullable<int> Credits { get; set; }
}
Se observǎ cǎ validarea funcționeazǎ, dar se realizeazǎ la nivel de server.
Existǎ și abordarea, mai generalǎ, de a se crea o clasǎ de validare unicǎ care sǎ valideze toate elementele (proprietǎțile) care implicǎ validare din cadrul unei anumite clase model. Într-o astfel de validare proprietǎțile de validat sunt transmise ca și parametri; în plus fațǎ de imlementarea anterioarǎ în cadrul clasei se mai implementeazǎ un constructor cu parametri prin intermediul cǎruia se transmit proprietǎțile de validat:
public class CreditePare:ValidationAttribute
{
//proprietatile de validat
public string Proprietate_Credite { get; set; }
//constructor cu parametri
public CreditePare(string proprietate_credite)
{
this.Proprietate_Credite = proprietate_credite;
}
protected override ValidationResult IsValid
(object value, ValidationContext validationContext)
{
//proprietatea de validat obtinuta din cotext
var credite =
validationContext.ObjectType.GetProperty(this.Proprietate_Credite);
//valoare proprietate
var valoare_credite =
credite.GetValue(validationContext.ObjectInstance, null);
if (valoare_credite is int)
{
if (((int)valoare_credite) != 2 && ((int)valoare_credite) != 4
&& ((int)valoare_credite) != 6)
return new ValidationResult("Creditele pot fi 2, 4 sau 6!");
return ValidationResult.Success;
}
else return new ValidationResult("Eroare de validare!");
}
}
Clasa se poate completa cu alte proprietǎți care se valideazǎ. Utilizarea se realizeazǎ transmitându-i proprietatea ca parametru:
[CreditePare("Credits")]
public class CreditsMetadata
{ }
II.4.3. Șablonul Repository
Aplicațiile web orientate cǎtre date necesitǎ abordǎri extreme de flexibile atunci când este vorba de accesul la date din cadrul acestora.
Figura 17: Accesul la baza de date fǎrǎ Repository
Șablonul Repository furnizeazǎ în acest sens o implementare care este centratǎ pe ideea izolǎrii dintre baza de date fizicǎ din spatele aplicației și logica de acces cǎtre baza de date și/sau interogǎrile efectuate asupra acesteia din cadrul aplicației.
Astfel, într-o aplicație care utilizeazǎ Entity Framework pentru accesul la date dar nu și șablonul Repository, accesul cǎtre baza de date se realizeazǎ pe baza contextului definit în cadrul clasei Controller (dupǎ cum a fost ilustrat în exemplificǎrile anterioare – Figura 17):
Controller -> DbContext -> Entity Framework -> Baza de date
În exemplul MyUniversityApp, acest context este reprezentat de clasa DBUnversityEntities:
namespace MyUniversityApp.Models
{
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
public partial class DBUniversityEntities : DbContext
{
public DBUniversityEntities()
: base("name=DBUniversityEntities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
}
}
Într-o astfel de abordare, metodele-acțiune din cadrul controller-ului conțin cod specific pentru realizarea operațiilor de acces la baza de date (CRUD) – vezi cap. II.2 Nivelul Controller – Clasele Controller:
public class StudentController : Controller
{
private DBUniversityEntities db = new DBUniversityEntities();
//
// GET: /Student/
public ActionResult Index()
{
return View(db.Students.ToList());
}
//
// GET: /Student/Details/5
public ActionResult Details(int id = 0)
{
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
…..// alte medode actiune
}
Metodele acțiune utilizeazǎ în mod direct instanța contextului de date creatǎ de Entity Framework (DBUniversityEntities db) și realizeazǎ operații de INSERT, UPDATE and DELETE a datelor utilizând DbSet. Pe baza codului implementat în metodele acțiune, Entity Framework transmite comezile cǎtre baza de date SQL Server.
Abordarea include codul de acces cǎtre baza de date (crearea contextului, scrierea de interogǎri, manipularea datelor) direct în corpul metodelor acțiune, ceea ce poate conduce la duplicarea codului. De exemplu, același cod de acces/modificare/adǎugare a unui student poate sǎ aparǎ în mai multe controllere, duplicându-se în mod inutil și complicând mentenanța ulterioarǎ atunci când sunt necesare schimbǎri, deoarece acestea ar trebui realizate în mai multe pǎrți ale aplicației.
a. Utilizarea unui Repository specific
Prin utilizarea unui Repository se introduce un nivel intermediar între domeniul aplicației (Controller) și nivelul care mapeazǎ datele (contextul DbContext), izolând codul de acces la date de restul aplicației. Acest lucru se realizeazǎ printr-o interfațǎ similarǎ cu cea a accesului la colecții de date și care furnizeazǎ metode specifice pentru fiecare tip de acces (SelectAll(), SelectById(), Insert(), Update(), Delete(), etc.). Atunci când se utilizeazǎ un Repository, clasele Controller nu vor interacționa cu Entity Framework în mod direct, ci numai prin intermediul unei instanțe a clasei Repository. Toate metodele acțiune ale controller-elor vor utiliza metodele clasei Repository pentru accesul la date în loc de a realiza accesul în mod direct; operațiile de acces create în clasa Repository utilizeazǎ, la rândul lor, contextul de date creat de Entity Framework pentru implementarea efectivǎ a operațiilor.
Astfel, orice schimbare în modalitatea de acces se realizeazǎ într-un singur punct – clasa Repository – iar testarea controller-elor devine mai simplǎ. Izolarea obținutǎ prin utilizarea unui Repository promoveazǎ reutilizarea și simplificǎ implementarea și testarea claselor Controller. Astfel, pornind de la varianta fǎrǎ Repository, ilustratǎ în Figura 17, includerea unui Repository ar implica implementarea unui nivel suplimentar între Controller și modelul reprezentat de contextual creat, prin intermediul cǎruia se realizeazǎ interacțiunea. Metodele definite în cadrul acestui Repository sunt utilizate în cadrul metodelor-acțiune defininte în Controller pentru a realize accesul la modelul de date.
Figura 18: Accesul la baza de date cu Repository
Pentru ilustrarea conceptului de Repository s-a considerat un exemplu de aplicație care presupune gestionarea unor produse (obiecte din clasa Produs). Aplicația utilizeazǎ ca și sursǎ de date o bazǎ de date DBProduse.mdf iar pentru produse avem tabela Produse pe baza cǎreia, utilizând DataBase First a fost generat modelul Produse.edmx:
Figura 19: Tabela Produse și modelul generat
, precum și contextul DBProduseEntities:
public partial class DBProduseEntities : DbContext
{
public DBProduseEntities()
: base("name=DBProduseEntities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public DbSet<Produse> Produses { get; set; }
}
Utilizând un Repository, datele aferente aplicației (produsele) sunt vǎzute sub forma unei liste din memorie, facilitând astfel schimbarea eventualǎ a sursei de date efectiv utilizate. Modelul de date al aplicației (produsul) este reprezentat printr-un obiect puternic tipizat din clasa Produse:
public partial class Produse
{
public long Id { get; set; }
public string Nume { get; set; }
public string CodIntern { get; set; }
public string Producator { get; set; }
public Nullable<int> Pret { get; set; }
}
Construirea Repository-ului are la bazǎ interfața IProdusRepository care definește operațiile de bazǎ care vor fi implementate pentru manipularea produselor:
interface IProdusRepository
{ IEnumerable<Produse> GetAll();
Produse Get(long id);
Produse Add(Produse elem);
void Remove(long id);
bool Update(Produse elem);
}
Clasa ProdusRepository implementeazǎ operațiile definite în cadrul interfeței:
public class ProdusRepository: IProdusRepository
{
//contextul creat de Entity Framework
private DBProduseEntities db = null;
public ProdusRepository()
{
this.db = new DBProduseEntities();
}
public ProdusRepository(DBProduseEntities db)
{
this.db = db;
}
public IEnumerable<Produse> GetAll()
{
return db.Produses.ToList<Produse>();
}
public Produse Get(long id)
{
return db.Produses.Find(id);
}
public Produse Add(Produse elem)
{
if (elem == null)
{
throw new ArgumentNullException("elem");
}
db.Produses.Add(elem);
//actualizeaza schimbarile in baza de date
db.SaveChanges();
return elem;
}
public void Remove(long id)
{
Produse exista = db.Produses.Find(id);
db.Produses.Remove(exista);
//actualizeaza schimbarile in baza de date
db.SaveChanges();
}
public bool Update(Produse elem)
{
if (elem == null)
{
throw new ArgumentNullException("elem");
}
Produse exista = db.Produses.Find(elem.Id);
if (exista ==null)
{
return false;
}
db.Produses.Remove(exista);
db.Produses.Add(elem);
//actualizeaza schimbarile in baza de date
db.SaveChanges();
return true;
}
}
Se observǎ existența a doi constructori: unul fǎrǎ parametru și unul care are ca parametru o instanțǎ a context-ului creat de Entity Framework. Acest al doilea constructor este util în situații când se dorește transmiterea unui context extern.
În cazul utilizǎrii unui Repository, clasa controller corespunzǎtoare va „consuma” serviciile (metodele) puse la dispoziție de cǎtre Repository, astfel încǎt toate detaliile legate de accesul cǎtre sursa de date sǎ fie ascunse în implementarea repository-ului:
public class ProduseController : Controller
{
//un camp instanta a IProdusRepository
static readonly IProdusRepository repository = new ProdusRepository();
public ActionResult Index()
{
List<Produse> model = (List<Produse>)repository.GetAll();
return View(model);
}
…// alte metode
}
Se observǎ existența unui câmp privat în cadrul clasei Controller, instanțǎ a clasei IProdusRepository. Fiecare metodǎ a controller-ului realizeazǎ accesul la date în mod indirect, prin intermediul metodelor clasei ProdusRepository.
Figura 20: IProdusRepository și clasa ProdusRepository
Diagrama de clase aferentǎ Repository-ului implementat în aceastǎ variantǎ implicǎ urmǎtoarea structurǎ de clase: interfața IProdusRepository și clasa care o implementeazǎ ProdusRepository și care furnizeazǎ API-ul care va fi utilizat în cadrul metodelor din clasa ProduseController (Figura 20).
b. Utilizarea unui Repository generic
Abordarea anterioarǎ de implementare a unui Repository implicǎ crearea unei clase Repository specifice pentru fiecare clasǎ model din cadrul aplicației (modelul Produse în acest caz). Dacǎ aplicația ar avea mai multe modele atunci fiecare ar dispune, într-o astfel de abordare, de câte un Repository propriu: abordare flexibilǎ care însǎ genereazǎ multe clase și duplicǎ codul deoarece existǎ un set de metode care se repetǎ în toate Repository-urile astfel implementate (de exemplu: GetAll, Add).
O altǎ abordare este de a utiliza un Repository generic care sǎ conținǎ metodele comune tuturor Repository-urilor din cadrul aplicației și sǎ derivǎm din acesta clase Repository în care sǎ implementǎm doar metodele specifice unui anumit tip de Repository. Aceastǎ abordare pǎstreazǎ flexibilitatea dar eliminǎ codul duplicat.
Astfel, implicațiile asupra structurii claselor sunt ilustrate în Figura 21. Se observǎ crearea unui repository generic, bazat pe o interfațǎ genericǎ pe baza cǎruia se creazǎ repository-ul pentru produse.
Figura 21: Diagrama de clase aferentǎ utilizǎrii unui Repository generic
Implementarea claselor necesare are la bazǎ clase/interfețe generice în C#. Clasa de bazǎ, genericǎ, pentru repository, RepositoryBaza, implementeazǎ interfața genericǎ IrepositoryBaza și împreunǎ definesc metodele comune tuturor Repository-urilor aplicației:
public interface IRepositoryBaza<TModel> where TModel:class
{
IQueryable<TModel> GetAll();
TModel Get(long id);
TModel Add(TModel elem);
void Remove(long id);
bool Update(TModel elem);
}
public class RepositoryBaza<TModel>: IRepositoryBaza<TModel> where TModel:class
{
private DBProduseEntities dbContext;
// setul de date din cadrul contextului – generic – Produses
protected DbSet<TModel> dbSet;
public RepositoryBaza()
{
this.dbContext = new DBProduseEntities();
//new DBProduseEntities();
this.dbSet = dbContext.Set<TModel>();
}
public RepositoryBaza(DBProduseEntities db)
{
this.dbContext = db;
this.dbSet = dbContext.Set<TModel>();
}
public IQueryable<TModel> GetAll()
{
return dbSet.AsQueryable<TModel>();
}
public TModel Get(long id)
{
return dbSet.Find(id);
}
public TModel Add(TModel elem)
{
if (elem == null)
{
throw new ArgumentNullException("elem");
}
dbSet.Add(elem);
//actualizeaza schimbarile in baza de date
dbContext.SaveChanges();
return elem;
}
public void Remove(long id)
{
TModel exista = dbContext.Set<TModel>().Find(id);
dbContext.Set<TModel>().Remove(exista);
//actualizeaza schimbarile in baza de date
dbContext.SaveChanges();
}
public bool Update(TModel elem)
{
if (elem == null)
{
throw new ArgumentNullException("elem");
}
TModel exista = dbSet.Find(elem);
if (exista ==null)
{
return false;
}
dbSet.Remove(exista);
dbSet.Add(elem);
//actualizeaza schimbarile in baza de date
dbContext.SaveChanges();
return true;
}
}
Clasa ProdusGenRepository va moșteni metodele Repository-ului generic RepositoryBaza și va adǎuga comportament specific implementând interfața IProdusGenRepository:
interface IProdusRepository
{
IQueryable<Produse> GetAll();
Produse Get(long id);
Produse Add(Produse elem);
void Remove(long id);
bool Update(Produse elem);
}
public class ProdusGenRepository:RepositoryBaza<Produse>, IProdusGenRepository
{
public IQueryable<Produse> GetByProducator(string producator)
{
return dbSet.AsQueryable<Produse>().
Where(p => string.Equals((p.Producator).ToString().Trim(), producator,
StringComparison.OrdinalIgnoreCase));
}
}
Un exemplu de utilizare a unui Repository-ului astfel definit în cadrul unui controller este prezentat în continuare în secțiunea Web API.
III. Web API
ASP.NET web API este un framework care permite crearea de servicii HTTP destinate unei largi game de clienți: browser-e, dispozitive mobile, etc. Reprezintǎ o platformǎ idealǎ pentru dezvoltarea de aplicații RESTful în .NET Framework.
Aplicatii RESTful
Principiile REST au ca principalǎ focalizare scalabilitatea aplicațiilor rezultate. În centrul REST sunt resursele care trebuie managerizate utilizând un API și care sunt expuse prin intermediul API-ului. Resursa are un URI care o identificǎ, și care nu este o reprezentare a resursei într-un anumit format. Formatul în care resursa este obținutǎ este negociatǎ între client și server: XML, JSON, HTML, etc. Clientul nu cunoaște decât URI-ul și reprezentarea resursei, nu deține informații despre locul unde resursa accesatǎ este stocatǎ.
Lipsa de stare reprezintǎ unui din conceptele fundamentale REST: serverul nu stocheazǎ niciodatǎ informații despre clienții sǎi. De asemenea, nu trebuie sǎ utilizeze sesiuni sau alte mecanisme de pǎstrare a stǎrii, cererea curentǎ nu este corelatǎ în niciun fel cu cele anterioare sau viitoare. Clientul, pe de altǎ parte, poate pǎstra informații despre resursǎ în cache, iar serverul trebuie sǎ furnizeze doar informații despre modul de stocare în cache.
Un alt aspect important al REST îl reprezintǎ faptul cǎ operațiile asupra resurselor au la bazǎ verbe HTTP utilizate în combinație cu URI. Cele mai utilizate verbe sunt GET și POST, dar lista poate fi completatǎ cu PUT, DELETE, HEAD, PATCH, OPTIONS, etc.
Principiile de mai sus se aplicǎ extrem de bine pentru aplicațiile web care, intrinsec, tind sǎ adere la REST. Crearea aplicațiilor web API este asistatǎ în Visual Studio prin introducerea unui template de proiect specific – ASP.NET MVC API template. Structura soluției rezultate este similarǎ cu cea de la MVC, deoarece web API utilizeazǎ șablonul MVC cu cele trei directoare specifice: Controllers, Models și Views. Directorul Views și altele precum Images, Scripts sau Content nu sunt utilizate însǎ în cadrul aplicațiilor de tip web API deoarece acestea sunt destinate returnǎrii de date și nu a unei interfețe (view) specifice.
Ca și în MVC, proiectele Web API utilizeazǎ un sistem de rutare iar configurarea rutelor este realizatǎ în fișierul WebApiConfig.cs din directorul App_Start, și poate avea urmǎtoarea formǎ:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Template-ul rutelor definite este similar cu cel din ASP.MVC și acestea definesc șabloul din intrare, implicit începând cu api:
"api/{controller}/{id}"
Rutele definite implicit în Visual Studio pot fi eventual schimbate, dacǎ se dorește.
În aplicațiile WB API, clasele Controller sunt derivate din ApiController și nu dispun de metodele acțiune cunoscute de la MVC (Index, …). Numele metodelor pot fi oricare însǎ prefixul acestora trebuie sǎ defineascǎ verbul prin intermediul cǎruia se identificǎ modalitatea în care se va manipula resursa: GET, POST, PUT, DELETE. Convențiile utilizate în acest sens se bazeazǎ pe existența acestor prefixe-verbe în numele metodelor definite în cadrul controllerelor:
Get…() – GET => returneazǎ o colecție de valori sau o valoare singularǎ
Post..() sau Put…() => insereazǎ sau modificǎ valoarea resursei
Delete..() => șterge resursa cu id-ul specificat
Metodele astfel definite sunt de fapt acțiuni care sunt apelate atunci când apare o cerere HTTP de tipul GET, POST, PUT sau DELETE. Se pot utiliza și atribute pentru a specifica selecția metodei, de exemplu [HttpGet] sau [HttpPost].
Dezvoltând exemplul care managerizeazǎ setul de produse utilizând un Repository specific (non-generic) ca și o aplicație Web API, o implementare a clasei ProduseController ar fi urmǎtoarea:
public class ProduseController : ApiController
{
//un camp instanta a IProdusRepository
static readonly IProdusRepository repository = new ProdusRepository();
//api/produse – HTTP GET
public IEnumerable<Produse> GetAllProducts()
{
return repository.GetAll();
}
//api/produse/id – HTTP GET
public Produse GetProduct(long id)
{
Produse elem = repository.Get(id);
if (elem == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return elem;
}
//api/produse?producator=producator
public IEnumerable<Produse> GetProduseByProducator(string producator)
{
return repository.GetAll().Where(
p => string.Equals((p.Producator).ToString().Trim(),
producator, StringComparison.OrdinalIgnoreCase));
// (p.Producator).ToString().Trim()
// elimina eventualele spatii din cimp pentru o comparatie corecta!
}
//adaugare produs – HTTP POST
//parametri de tip complex sunt deserializati din corpul cererii HTTP – //clientul trimite de obicei XML sau JSON
[HttpPost]
public Produse PostProdus(Produse elem)
{
elem = repository.Add(elem);
return elem;
}
//modificare produs – HTTP PUT
[HttpPut]
public void PutProdus(Produse produs)
{
if (!repository.Update(produs))
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}
//stergere produs – HTTP DELETE
[HttpDelete]
public void DeleteProdus(long id)
{
Produse elem = repository.Get(id);
if (elem == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
repository.Remove(id);
}
}
Controller-ul expune metode specifice pentru realizarea operațiilor de bazǎ asupra sursei de date (CRUD). Fiecare metodǎ are o semnǎturǎ proprie.
În cazul în care s-ar utiliza varianta de implementare bazatǎ pe Repository-ul generic, metodele implementate în cadrul controller-ului sunt aceleași, deci din punctul de vedere al controller-ului nu s-a schimbat nimic, doar cǎ în loc de:
//un camp instanta a IprodusRepository – vechiul repository
static readonly IProdusRepository repository = new ProdusRepository();
acesta va utiliza noul repository :
//un camp instanta a IProdusGenRepository – noul repository
static readonly IProdusGenRepository repository = new ProdusGenRepository();
Pentru consumarea API-ului creat au fost create o serie de funcții JavaScript care utilizeazǎ Ajax și jQuery care utilizeazǎ API-ul: funcții care returneazǎ toate produsele, un anumit produs dupǎ id sau producǎtor și respectiv actualizeazǎ un produs:
var uri = 'api/produse';
// se declanseaza actiunea GetAllProducts asociata uri_ului api/produse
$(document).ready(function () {
// trimite cerere AJAX
$.getJSON(uri)
.success(function (data) {
// la success, 'data' contine lista produselor
$.each(data, function (key, item) {
// Add a list item for the product.
$('<li>', { text: formatItem(item) }).appendTo($('#produse'));
});
});
});
// se controleazǎ formatul elementelor
function formatItem(item) {
return item.Id + ' ' + item.Nume + ' – ' + item.Producator + ': RON '
+ item.Pret + ' '+ item.CodIntern ;
}
// se declanseaza actiunea GetProduct asociata uri_ului api/produse/id
function find() {
var id = $('#prodId').val();
$.getJSON(uri + '/' + id)
.success(function (data) { // la success, 'data' contine produsul
$('#produs').text(formatItem(data));
})
.error(function (jqXHR, textStatus, err) {
$('#produs').text('Error: ' + err);
});
}
// se declanseaza actiunea GetProduseByProducator asociata uri_ului
// api/produse?producator=producator
function findP() {
var producator = $('#prodPROD').val();
alert(producator);
$.getJSON(uri + '?producator=' + producator)
.success(function (data) { // la success, 'data' contine produsul
alert(data);
$.each(data, function (key, item) {
// adauga o lista cu produsele selectate.
$('<li>', { text: formatItem(item) }).appendTo($('#producator'));
});
alert("success…" + data);
})
.error(function (jqXHR, textStatus, err) {
$('#producator').text('Error: ' + err);
alert("error…" + data);
});
}
//adaugare produs – se declanseaza actiunea HTTP POST
function AddProdus() {
//un nou produs – notatia JSON
var produsData = {
"Id": "2567",
"Nume": "Router",
"CodIntern": "ROUTCIS",
"Producator": "Cisco",
"Pret": "800"
};
//transmitem noul produs in format JSON – cerere HTTP POST
$.post(uri,produsData)
//la succes, data va contine produsul adaugat
.success(function (data, status, jqXHR) {
alert("success…" + data);
})
.error(function (xhr) {
alert("error…" + data);
alert(xhr.responseText);
});
}
Afișarea elemenetelor obținute se realizeazǎ utilizând pagina Index.cshtml a aplicației Web API în cadrul cǎreia a fost modificatǎ secțiunea <body> astfel:
<div id="body">
<section class="featured">
<div class="content-wrapper">
<hgroup class="title">
<h1>TEST ASP.NET Web API!</h1>
<h2>Operatii de baza client web API.</h2>
</hgroup>
</div>
</section>
<section class="content-wrapper main-content clear-fix">
<h3>Client pentru API – Lista produselor:</h3>
<div>
<h2>Toate produsele</h2>
<ul id="produse" />
</div>
<<div>
<h2>Cautare produs dupa ID</h2>
<input type="text" id="prodId" size="5" />
<input type="button" value="Search" onclick="find();" />
<p id="produs" />
</div>
<div>
<h2>Cautare produs dupa producator</h2>
<input type="text" id="prodPROD" size="5" />
<input type="button" value="Search" onclick="findP();" />
<ul id="producator" />
</div>
<div>
<h2>Adaugare produs</h2>
<input type="button" value="Adauga" onclick=" AddProdus ();" />
</div>
</section>
</div>
Adǎugarea unui nou produs este hard-coded, cu titlu de exemplificare. Se utilizeazǎ metoda jQuery $.getJSON pentru a realiza cereri GET cǎtre serviciul API /api/produse respectiv /api/produse/id și api/produse?producator=producator care returneazǎ o colecție de produse serializatǎ în format JSON respective un produs (dupǎ id sau producator). De asemenea, se utilizeazǎ metoda jQuery $.post pentru a realiza o cerere POST cǎtre serviciul /api/produse pentru a adǎuga un nou produs.
OData – Open Data – este un protocol web proiectat de MicroSoft destinat interogǎrilor și actualizǎrilor de date peste HTTP. ASP.NET Web API are integrat suportul pentru OData începând cu Visual Studio 2012 (sau utilizând pachetul MicroSoft ASP.NET Web API OData NuGet) astfel încât pot fi interogate resurse utilizând sintaxa OData.
O importantǎ aplicabilitate a protocolului în cadrul framework-ului Web API o reprezintǎ posibilitatea realizǎrii paginǎrii și filtrǎrii datelor. Pentru a realiza însǎ acest lucru, acțiunea din cadrul controller-ului care returneazǎ rezultatele care trebuiesc paginate/filtrate trebuie sǎ returneze un rezultat de tipul IQueryable<T> și nu IEnumerable<T>. Transformarea rezultatului se poate realiza utilizând extensia LINQ AsQueryable(). De exemplu, metoda GetAllProducts() ar arǎta în felul urmǎtor:
public IQueryable<Produse> GetAllProducts()
{
return repository.GetAll().AsQueryable();
}
Celelalte metode se pot adapta în mod analog.
V. Optimizarea aplicațiilor web
Tehnicile de optimizare a aplicațiilor web au ca principal scop creșterea responsivitǎții aplicațiilor (prin îmbunǎtǎțirea timpului de încǎrcare a paginilor) chiar și în condiții de lǎțime de bandǎ limimtate.
În general, factorii care influențeazǎ timpul de încǎrcare a unei pagini web sunt în strânsǎ legǎturǎ cu structura paginii care, de obicei conține elemente precum cod HTML, fișiere JavaScript, imagini și/sau conținut media (Flash sau Silverlight). La încǎrcarea paginii, browser-ele randeazǎ pagina parcurgând într-o manierǎ de tipul top-down fișierul HTML corespunzǎtor acesteia: resursele conținute în cadrul paginii (imagini, fișiere CSS/JavaScript, etc.) sunt descǎrcate pe mǎsurǎ ce apar și randarea se finalizeazǎ doar atunci când toate resursele au fost descǎrcate. În consecințǎ, tehnicile de optimizare a încǎrcǎrii paginilor sunt axate pe reducerea numǎrului de resurse precum și rearanjarea acestora în cadrul paginii.
Importanța tehnicilor de optimizare utilizate pentru îmbunǎtǎțirea performanțelor de încǎrcare a paginilor web este recunoscutǎ de cei mai mai jucǎtori din domeniu: atât Yahoo! cât și Google au identificat reguli de bune practici în acest sens, disponibile la https://developer.yahoo.com/performance/rules.html și respectiv https://developers.google.com/speed/docs/insights/rules
Dintre acestea, cele mai cunoscute și utilizate practici sunt urmǎtoarele:
V.1. Minimizarea numǎrului de cereri HTTP prin reducerea numǎrului de resurse
ASP.NET MVC 4(5) și NET 4.5 prezintǎ suport pentru Bundling and Minification prin care oferǎ posibilitatea de a grupa (bundling) diferite resurse într-una singurǎ precum și pentru a minimiza dimensiunea acestora (minification).
Acesta se bazeazǎ pe bibliotecǎ System.Web.Optimization și pe metodele helper furnizate în cadrul acesteia: Styles și Scripts. Un exemplu de utilizare se gǎsește în fișierul _Layout.cshtml:
//la începutul paginii
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
….
//la sfârsitul paginii
@Scripts.Render("~/bundles/jquery")
Metoda Render utilizeazǎ o cale virtualǎ cǎtre conținutul care trebuie randat.
Fișierul modernizr.js este singurul fișier JavaScript randat la începutul paginii deoarece rolul sau este acela de a detecta capabilitǎțile (video/audio, HTML5) suportate de browser. În rest, principiul consacrat implicǎ randarea fișierelor de stil la început și fișierelor JavaScript la sfârșitul paginii.
Bundling and minification îmbunǎtǎțește timpul de încǎrcare a paginii prin reducerea numǎrului de cereri cǎtre server precum și a dimensiunii acestora.
Bundling permite gruparea mai multor fișiere (JavaScript, CSS) într-unul singur implicând astfel o singurǎ cerere HTTP cǎtre server.
Minification realizeazǎ diverse prelucrǎri asupra fișierului transmis în scopul reducerii dimensiunii acestuia (extragerea spațiilor și a comentariilor care nu sunt necesare, scurtarea numelor variabilelor, etc.). Cele mai populare unelte pentru minimizarea codului JavaScript sunt JSMin și respectiv YUI (folosit și pentru CSS). Mai existǎ și tehnica numitǎ obfuscation (aplicatǎ codului) ca și alternativǎ la minification, cu ratǎ de minimizare mai bunǎ dar a cǎrei utilizare s-a dovedit mai sensibilǎ la erori în procesul de minimizare.
În .NET Framework se pot defini de asemenea bundle-uri apelând BundleCollection.Add() (din System.Web.Optimization) și transmițându-i o instanțǎ ScriptBundle sau StyleBundle. Dupǎ definire, acestea trebuie și înregistrate. Acest lucru se realizeazǎ în fișierul BundleConfig.cs din directorul App_Start:
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include(
"~/Scripts/jquery-ui-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
"~/Scripts/jquery.unobtrusive*",
"~/Scripts/jquery.validate*"));
…..
}
Sunt incluse fișierere din diretctorul Scripts al aplicației.
V.2. Compresia GZip
O altǎ posibilitate de a reduce timpul de descǎrcare a paginilor implicǎ compresia componentelor din cadrul acestora, în general utilizând compresie GZip. Serverele web pot fi configurate astfel încât sǎ recunoascǎ conținut în formǎ comprimatǎ (HTML, JavaScript, CSS). Compresia nu se recomandǎ în cazul componentelor binare (imagini, audio, pdf) deoarece acestea sunt deja comprimate și o recomprimare ar putea chiar mǎri dimensiunea acestora.
V.3. Utilizarea unui CDN (Content Delivery Network)
CDN reprezintǎ o colecție de servere web distribuite în mai multe locații în scopul de a furniza conținut web utilizatorilor într-o manierǎ cât mai eficientǎ: selectarea unui anumit server la un anumit moment dat are la bazǎ proximitatea în cadrul rețelei în scopul reducerii timpului de download al resurselor solicitate în cadrul unei pagini web. De regulǎ browser-ele limiteazǎ numǎrul de conexiuni pentru descǎcarea resurselor la nivel de domeniu (în general la 2 conexiuni – eventual ceva mai multe pentru browser-ele mai noi). Împǎrțirea resurselor pe domenii multiple reprezintǎ de asemenea o modalitate de creștere a eficienței la descǎrcare, aceasta putând avea loc în paralel, pentru fiecare domeniu, mǎrindu-se astfel numǎrul de conexiuni posibile.
Totuși nu se recomandǎ utilizarea unui numǎr mare de domenii unice în cadrul unei aplicații deoarece procesul de cǎutare a acestora implicǎ timp suplimentar; pe de altǎ parte, reducerea numǎrului de domenii reduce posibilitatea de a avea mai multe descǎrcǎri în paralel. De regulǎ, se recomandǎ cel mult 2-4 domenii pentru o balansare optimǎ.
V.4. Poziționarea fișierelor de stil la începutul paginii (secțiunea head)
Poziționarea fișierelor de stil la începutul paginii asigurǎ încǎrcarea acestora la început și dǎ posibilitatea paginii de a se putea încǎrca progresiv; astfel, pe parcursul încǎcarii vom putea avea un feedback vizual al modalitǎții de încǎrcare, începând cu header-ul cǎtre conținutul și footer-ul paginii. Dacǎ fișierele de stil s-ar poziționa la sfârșit, componentele paginii nu ar putea fi randate decât dupǎ ce se cunoaște stilul care li se aplicǎ, deci pagina nu ar putea fi vizualizatǎ decât la final; de altfel, multe browser-e blocheazǎ randarea elementelor din paginǎ pânǎ la încǎrcarea fișierelor de stil aferente acestora.
V.5. Poziționarea script-urilor la baza paginii
Justificarea acestei abordǎri are la bazǎ faptul cǎ atunci când se descarcǎ un script, acest lucru blocheazǎ alte download-uri care ar putea sǎ se realizeze în paralel, chiar dacǎ este vorba de domenii diferite. Dacǎ poziționarea la baza paginii a script-urilor nu e posibilǎ, existǎ o serie de tehnici prin care sǎ se trateze asfel de situații; de exemplu, utilizarea tehnicii lazy loading (Gmail). Practic, se amânǎ descǎrcarea script-urilor (browser-ul ignorǎ porțiunea respectivǎ și continuǎ randarea paginii) pânǎ când modulele din cadrul script-ului respectiv sunt necesare în urma unei acțiuni a utilizatorului.
V.6. Fișierele script și css sǎ fie externe
Includerea stilurilor și a scripturilor în fișiere externe și evitarea includerii acestora inline în documentul HTML permite browser-elor sǎ le memoreze în cache ceea ce conduce la o mai rapidǎ încǎrcare ulterioarǎ a paginii. Pe de altǎ parte, includerea inline conduce la un numǎr mai mic de resurse care trebuie descǎrcate (și implicit un numǎr mai mic de cereri HTTP) deoarece totul se descarcǎ într-o singurǎ cerere.
Ambele variante – reducerea numǎrului de conexiuni respectiv memorarea în cache au avantaje în diferite contexte:
dacǎ aplicația reutilizeazǎ resurse la nivelul mai multor pagini, utilizarea cache-ului poate aduce certe avantaje
dacǎ aplicația are relativ puține pagini și utilizeazǎ resurse diferite pentru diferitele pagini, randarea lor inline poate fi avantajoasǎ
Se pot utiliza și abordǎri combinate: încǎrcarea paginii inițiale inline și a resurselor externe în mod dinamic utilizând Ajax.
V.7. Utilizarea cache-ului în aplicațiile web
Utilizarea cache-ului în aplicațiile web reprezintǎ una din tehnicile cele mai eficiente de îmbunǎtǎțile a performanțelor. Problema care se pune în acest caz este gǎsirea informațiilor relevante (conținutul) care trebuie pǎstrate în cache precum și a duratei pǎstrǎrii acestora. Tehnicile de caching utilizate la nivel de server urmǎresc tratarea într-o manierǎ cât mai eficientǎ a cererilor memorarea la nivel de server pe când tehnicile de caching la nivel de client urmǎresc evitarea realizǎrii de cereri cǎtre server și memorarea se realizeazǎ la nivelul calculatorului clientului.
a.Tehnici server-side caching
În contextul aplicațiilor ASP.NET MVC existǎ o serie de tehnici dedicate și care, în principal diferǎ prin domeniul de valabilitate al informațiilor care se stocheazǎ în cache:
la nivel de cerere (request) – pentru datele relevante cererii curente. Deoarece fiecare cerere ASP.NET creazǎ o instanțǎ a obiectului System.Web.HttpContext, proprietatea Items poate fi utilizatǎ pentru a stoca date la nivelul cererii. Proprietatea poate fi utilizatǎ ca un dicționar (IDictionary):
HttpContext.Items["PrimaLogare"] = true;
bool prima_logare = (bool)HttpContext.Items["PrimaLogare"];
la nivel de utilizator (user) – ASP.NET Session State implicǎ memorarea datelor care persistǎ de-a lungul tuturor cererilor realizate de cǎtre un utilizator. Atunci când sesiunea este activatǎ, se poate utiliza proprietatea HttpContext.Session a obiectului System.Web.HttpContext pentru a salva informațiile în cadrul sesiunii, pentru o viitoare cerere realizatǎ de cǎtre același utilizator. Session este de asemenea un dicționar netipizat; obiectele stocate în cadrul acestuia sunt pǎstrate pânǎ când sesiunea este încheiatǎ sau este distrusǎ de sever (timeout – inactivitate user). Valoarea implicitǎ timeout (20 minute) se poate modifica în fișierul Web.config prin modificarea atributului system.Web/SessionState timeout:
<system.web>
<sessionState timeout="10" />
</system.web>
la nivel de aplicație, prin:
proprietatea HttpContext.Application a obiectului System.Web.HttpContext este un dicționar care disponibil la nivel de aplicației. De fapt, valabilitatea informațiilor stocate la acest nivel este funcție de existența procesului Internet Information Server worker care gǎzduiește instanța curentǎ a aplicației.
HttpContext.Application["start"] = DateTime.Now;
DateTime startApp = (DateTime)HttpContext.Application["start"];
o alternativǎ mai bunǎ pentru stocarea datelor la nivel de aplicație o reprezintǎ ASP.NET cache prin HttpContext.Cache . Aceastǎ abordare este similarǎ cu cea furnizatǎ de HttpContext.Application dar eliminǎ problemele legate de procesele IIS. În plus, ASP.NET gestioneazǎ automat ștergerea din cache și notificǎ aplicația în acest sens. Ștergerea din cache poate apǎrea în situații precum: elementul din cache a expirat, dependențele elementului s-au schimbat și necesitǎ actualizare sau serverul nu mai are resurse de memorie.
Expirarea unui element din cache poate fi precizatǎ în ASP.NET în douǎ moduri:
sliding expiration – se specificǎ la cât timp dupǎ ce a fost accesat elementul acesta expirǎ
absolute expiration – se specificǎ un moment de timp
ASP.NET definește de asemenea o politicǎ de specificare/gestionare a dependențelor pentru elementele memorate în cache; dintre tipurile de dependențe care se pot seta cele mai importante sunt urmǎtoarele: File (dependențǎ de un fișier extern), Key (dependențǎ de un alt element din cache identificat prin cheie), SQL (dependențǎ de un table dintr-o bazade date MS SQL), Aggregate (combinǎ multiple dependențe), etc.
Scavenging reprezintǎ procesul de ștergere a unui element din cache atunci când resursele de memorie sunt slabe; ștergerea se realizeazǎ pe baza unor prioritǎți date elementelor funcție de timpul scurs de la ultima accesare.
Output caching – pe lângǎ tehnicile de caching menționate anterior care sunt focalizate pe stocarea datelor, ASP.NET permite stocarea în cache a codului HTML generat în cadrul unei cereri prin facilitatea denumitǎ output caching. Se utilizeazǎ în acest sens atributul OutputCacheAttribute asociat unei acțiuni a unui controller pentru a pǎstra în cache rezultatul acțiunii respective.
[OutputCache(Duration = 30, VaryByParam = "none")]
public ActionResult Contact()
{
ViewBag.Message = DateTime.Now.ToString();
return View();
}
Rezultatul este cǎ ViewBag.Message este actualizat doar dupa 30 secunde. Atributul OutputCache permite configurarea modului în care se realizeazǎ memorarea în cache prin intermediul parametrilor acestuia, ca de exemplu:
NoStore – valoarea true activeazǎ posibilitatea de a activa stocarea într-o locație secundarǎ
Duration – indicǎ durata de valabilitate a cache-ului în secunde
Location – specificǎ locația unde se memoreazǎ conținutul cache-ului: Client, Server, Any, None; alegerea trebuie realizatǎ funcție de context: de exemplu, dacǎ se memoreazǎ
user-ul clientului se va utiliza memorare la Client; utilizarea opțiunii implicite Any în acest caz va rezulta în afișarea numelui primului utilizator care a cerut pagina respectivǎ pentru toți utilizatorii, ceea ce nu este de dorit
VaryByParam – creazǎ în memoria cache diferite versiuni ale aceluiași conținut funcție de parametri acțiunii; similar, existǎ și VaryByHeader, VaryByCustom
[OutputCache(Duration = 60, VaryByParam = "id",
Location = OutputCacheLocation.Client,
NoStore = true)]
public ActionResult Details(int id)
{
…
return View(student);
}
În loc de a a încǎrca fiecare acțiune a unui controller cu atributul OutputCache, se pot crea reguli generale pentru output caching pe baza unor profiluri definite în fișierul Web.Config, în secțiunea <system.web>:
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="ProfilAppCache" enabled="true" duration="60"/>
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
Existǎ de asemenea și posibilitatea de a realiza doar un output cache parțial, pantru paginile cu conținut dinamic: întreaga paginǎ este stocatǎ în cache cu excepția unor porțiuni specifice care rǎmân dinamice. Tehnica este complexǎ, scenarii mai avansate pot fi implementate utilizând pachetul MvcDonutCaching NuGet package.
B. Tehnici Client-Side Caching
Sunt realizate prin intermediul browser-ului care stocheazǎ resursele local, pe hard-disk-ul calculatorului client, într-o zonǎ prealocatǎ de dimensiune predefinitǎ. Dimensiunea poate fi controlatǎ de utilizator.
Noile facilitǎți introduse de HTML5 și-au pus amprenta în mod deosebit asupra tehnicilor de tipul client-side caching, prin mecanisme noi și flexibile prin care clientul dispune de mai mult control vizavi de stocarea datelor în cache care tind pânǎ la posibilitatea de a lucra cu aplicația în lipsa conexiunii la Internet.
HTML 5 Application Cache – HTML5 introduce API-ul Application Cache prin care o aplicație web poate fi stocatǎ în cache pentru a fi accesibilǎ ulterior fǎrǎ o conexiune Internet disponibilǎ. Prezintǎ o serie de avantaje:
disponibilitatea aplicației și atunci când aplicația e offline
vitezǎ – resursele se încarcǎ mai rapid (local)
reducerea încǎrcǎrii serverului – descǎrcarea de pe server va avea loc doar în situația în care resursele au suferit modificǎri
Se realizeazǎ efectiv prin definirea unui fișier manifest (cache manifest file) de tip text în cadrul cǎruia se specificǎ browser-ului ce sǎ pǎstreze sau ce sǎ nu pǎstreze în cache. Fișierul manifest are 3 secțiuni:
CACHE MANIFEST – se specificǎ fișierele care vor fi pǎstrate înc cahe dupǎ prima descǎrcare
NETWORK – se specificǎ fișierele care necesitǎ o conexiune cǎtre server și deci nu vor fi pǎstrate în cache
FALLBACK – se specificǎ fișierul care va fi afișat dacǎ o paginǎ nu e accesibilǎ
Internet Explorer 10, Firefox, Chrome, Safari și Opera prezintǎ suport pentru Application cache. Browser-ele limiteazǎ de obicei dimensiunea pentru datele pǎstrate în cache
(de ex. 5MB per site web).
Pentru a referi fișierul manifest trebuie utilizat atributul manifest în documentul html. Browser-ul recunoaște faptul cǎ aplicația definește un fișier cache manifest și îl descarcǎ automat.
<!DOCTYPE html>
<html manifest="site.manifest">
…
</html>
Local Storage – utilizând API-ul HTML Local Storage paginile web pot sǎ stocheze date local, în cadrul browser-ului client. Reprezintǎ o versiune mai avansatǎ a cookie-urilor, mai sigurǎ și mai rapidǎ:
datele nu sunt incluse cu fiecare cerere, numai când sunt cerute
se pot stoca cantitǎți mari de date fǎrǎ a afecta performanța aplicației. Limita de stocare este mult mai mare decât era în cazul cookie-urilor (5MB). Datele sunt stocate sub forma de perechi nume-valoare.
API-ul Local Storage API utilizeazǎ douǎ obiecte pentru a realiza stocarea: localStorage și sessionStorage. Acestea sunt similare în termenii metodelor expuse, dar pentru localStorage datele sunt disponibile un timp nelimitat pe când pentru sessionStorage sunt disponibile pânǎ la închiderea sesiunii (documentului) curente. ocal Storage se bazeazǎ pe o structurǎ de tip dicționar pentru memorarea elementelor sub formǎ de perechi cheie-valoare. De exemplu, pentru stocarea unui element se utilizeazǎ metoda setItem:
localStorage.setItem("limba", "romana");
sau
localStorage["limba"] = "romana";
Pentru regǎsirea unui element se utilizeazǎ metoda getItem:
var limba = localStorage.getItem("limba");
Ștergere unui element sau a tuturor din cache se realizeazǎ utilizând metodele removeItem respectiv clear:
localStorage.removeItem("limba");
localStorage.clear();
Browser-ele actuale care oferǎ support pentru Local storage sunt Internet Explorer 8+, Firefox, Opera, Chrome, și Safari.
Testare aplicatie
Bibliografie
http://msdn.microsoft.com/en-us/magazine/hh781022.aspx
http://www.asp.net/mvc/tutorials/getting-started-with-ef-5-using-mvc-4/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application
http://www.asp.net/mvc/tutorials/mvc-5/database-first-development/creating-the-web-application
http://www.codeproject.com/Tips/732449/Understanding-and-Extending-Controller-Factory-i
http://www.codeproject.com/Articles/599189/DefaultControllerFactory-in-ASP-NET-MVC
http://www.bipinjoshi.net/articles/20e546b4-3ae9-416b-878e-5b12434fe7a6.aspx
Bibliografie
http://msdn.microsoft.com/en-us/magazine/hh781022.aspx
http://www.asp.net/mvc/tutorials/getting-started-with-ef-5-using-mvc-4/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application
http://www.asp.net/mvc/tutorials/mvc-5/database-first-development/creating-the-web-application
http://www.codeproject.com/Tips/732449/Understanding-and-Extending-Controller-Factory-i
http://www.codeproject.com/Articles/599189/DefaultControllerFactory-in-ASP-NET-MVC
http://www.bipinjoshi.net/articles/20e546b4-3ae9-416b-878e-5b12434fe7a6.aspx
Copyright Notice
© Licențiada.org respectă drepturile de proprietate intelectuală și așteaptă ca toți utilizatorii să facă același lucru. Dacă consideri că un conținut de pe site încalcă drepturile tale de autor, te rugăm să trimiți o notificare DMCA.
Acest articol: Model View Controller (ID: 122391)
Dacă considerați că acest conținut vă încalcă drepturile de autor, vă rugăm să depuneți o cerere pe pagina noastră Copyright Takedown.
