• ORM
  • Add-Migration ... en Update-Database
  • Data-annotaties: configureren data model en mapping
  • DBContext
    • DbSet: wat moet er opgeslagen worden?
    • OnConfiguring: connectiestring, (lazy) loading
    • OnModelCreating: configureren data model en mapping met Fluent API
  • Versie 5 ondersteunt many-to-many (release date)
  • De static class DBInitializer (is geen singleton)
  • ORM is geen pattern
  • MVC is een pattern
  • lazy load is een pattern
  • We kunnen een webapplicatie maken met ASP.NET Core MVC [10b]
  • We kunnen een database aanmaken code-first en gebruiken met de ORM EF Core [11b]
    (en bevragen met LINQ [11a])

Hoe kunnen we EF Core gebruiken in ASP.NET Core MVC?

  • Wanneer wordt de DBContext aangemaakt?
  • Wanneer wordt de in een HTTP request meegegeven data gechecked voordat het de database in gaat?
  • Richtlijn: maak een DBContext aan per unit of work
  • Hoe vaak SaveChanges, hoe vaak DBContext.Dispose?
  • SaveChanges:
  • Wat doet SaveChanges?
    • SaveChanges maakt een transactie aan en voert het uit.
  • De constructor en de Dispose van de DBContext: maak connectie en ruim op.
    • DBContext is niet thread-safe.
EF Core implementeert optimistic concurrency control. Zie de MSDN Transacties kan ook handmatig Vraag: wanneer is optimistic concurrency control beter?
Demo

In ASP.NET Core gebeurt dat niet in de controller.

  • Via dependency injection
  • Dependency injection is een design pattern
Verschillende 'vormen': door constructor, property, method, etc.
  • Dependency Inversion is een principe
  • Dependency Injection is een design pattern om Dependency Inversion te bereiken.
public class AudiRS2 { public void GeefGas() { /* ... */ } } public class Automobilist { private AudiRS2 auto; public Automobilist() { auto = new AudiRS2(); } public void Rijden() { // ... auto.GeefGas(); // ... } } public interface IAuto { void GeefGas(); } public class AudiRS2 : IAuto { public void GeefGas() { /* ... */ } } public class Automobilist { private IAuto auto; public Automobilist(IAuto auto) { this.auto = auto; } public void Rijden() { // ... auto.GeefGas(); // ... } } public interface IService { void DoIets(); } public class ConcreteService : IService { public void DoIets() { /* ... */ } } public class Client { private IService service; public Client(IService service) { this.service = service; } public void EenMethode() { // ... service.DoIets(); // ... } }
  • DIP kan ook met factory
  • Kan ook object meegeven aan methode of property setten.

In ASP.NET Core gebeurt dat niet in de controller. Lees dit.

  • Via dependency injection
  • Dependency injection is een design pattern
  • In Startup.cs: public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddDbContext<MijnContext>( options => options.UseSqlServer("...")); } En: public class MijnContext : DbContext { public MijnContext(DbContextOptions<MijnContext> p) : base(o) { } } En dan gebruiken we het als: public class MijnController { private readonly MijnContext _context; public MijnController(MijnContext context) { _context = context; } }
  • Deze dependency injection gaat op een ASP.NET Core-specifieke manier.
public interface IDateTime { DateTime Now { get; } } public class SystemDateTime : IDateTime { public DateTime Now { get { return DateTime.Now; } } } public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IDateTime, SystemDateTime>(); services.AddControllersWithViews(); } } public class HomeController : Controller { private readonly IDateTime _dateTime; public HomeController(IDateTime dateTime) { _dateTime = dateTime; } public string Index() { return "De tijd is: " + _dateTime.Now.ToString(); } }

Zie deze blog voor meer details.

Opties injecteren gaat met .Configure, zie de MSDN.

public class Movie { public int Id { get; set; } [StringLength(60, MinimumLength = 3)] [Required] public string Title { get; set; } [Display(Name = "Release Date")] [DataType(DataType.Date)] public DateTime ReleaseDate { get; set; } [Range(1, 100)] [DataType(DataType.Currency)] [Column(TypeName = "decimal(18, 2)")] public decimal Price { get; set; } [RegularExpression(@"^[A-Z]+[a-zA-Z]*$")] [Required] [StringLength(30)] public string Genre { get; set; } [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")] [StringLength(5)] [Required] public string Rating { get; set; } } // GET: Movies/Create public IActionResult Create() { return View(); } // POST: Movies/Create [HttpPost] public void IActionResult Create(Movie movie) { if (ModelState.IsValid) { _context.Add(movie); _context.SaveChanges(); return RedirectToAction("Index"); } return View(movie); }
  • Een Unit is klein
  • Meestal kiezen we als unit voor 1 methode van een specifieke Class
  • We willen graag een Automatische test voorbereiden
  • Dat is nuttig voor de bewaking van de kwaliteit van je code
  • Na iedere (kleine) wijziging kun je de code automatisch testen
  • Het voorkomt bugs in bestaande functionaliteit (omdat unittests moeten blijven slagen)
  • Voor nieuwe functionaliteit moeten er natuurlijk wel nieuwe unittests worden aangemaakt

Gecategoriseerd op 'grootte':

  • Unittest
  • Integratietest
  • Systeemtest

Gecategoriseerd op wat er getest wordt:

  • Compatibility testen. Bijv.: verschillende browsers
  • Performance testen / Load testen. Bijv.: voeg 10000 studenten toe en gebruik een timer.
  • Usability/accessibility testen (gastcollege Stichting Accessibility!)
  • Penetration testen (gastcollege Centric!)
  • Code quality testen. Bijv. tool om statisch code te checken: Microsoft.CodeAnalysis.FxCopAnalyzers NuGet package

[zie de MSDN voor meer uitleg]

Herinner onze DbInitializer:

public static class DbInitializer { public static void Initialize(MijnContext context) { if (context.Studenten.Any()) return; context.Studenten.Add(new Student { Naam = "Bob" }); context.Studenten.Add(new Student { Naam = "Alice" }); context.SaveChanges(); } }
public static void Main(string[] args) { var host = CreateHostBuilder(args).Build(); using (var scope = host.Services.CreateScope()) { DbInitializer.Initialize(scope.ServiceProvider .GetRequiredService<MijnContext>()); } host.Run(); }

In de configuratie van het model, OnModelCreating, kan ook worden geseed:

modelBuilder.Entity<Blog>().HasData(new Blog {BlogId = 1, Url = "http://sample.com"});

Dit is langzaam en Id's moeten worden geset.

  • Wat kun je eigenlijk testen in een Controller?
  • Je kunt alle Action-methods testen
  • Voor elke Action-Method schrijf je minimaal 1 unittest
  • Wat heb je nodig om de unittests voor te bereiden?
  • Je maakt een nieuw ASP.NET Core-project aan (de default is goed genoeg)
  • Daarna voeg je een testproject o.b.v. xUnit toe (zoals in 10a)
  • Vergeet niet om in je testcode een Reference op te nemen naar de webapplicatie (we testen namelijk de webapplicatie)

Hier kunnen we de Privacy Action-method voor gebruiken (aangemaakt in de Home-Controller)

[Fact] public void PrivacyInHomeController() { HomeController c = new HomeController(); var result = c.Privacy(); //result moet een view zijn var viewResult = Assert.IsType<ViewResult>(result); //checken op correcte ViewData var message = viewResult.ViewData["Message"]; Assert.Equal("Uw privacy wordt gerespecteerd. ", message); }
  • Om de Controller te kunnen gebruiken heb je een using-regel nodig in je unittest bestand using Project.Controllers;
  • Nu krijg je echter een foutmelding van de compiler voor de regel var result = c.Privacy();
  • Dit komt omdat de Privacy methode een IActionResult teruggeeft die hij niet kent
    • Voeg daarom eerst package Microsoft.AspNetCore.MVC toe
    • Gebruik dezelfde versie als in je echte project en neem using op in je testfile: using Microsoft.AspNetCore.Mvc;
    • NB: Soms is het nodig om je project opnieuw te starten (als foutmeldingen niet verdwijnen)
  • Soms zijn er problemen met het toevoegen van NuGet Package Microsoft.AspNetCore.Mvc
  • In Project.csproj file worden alle referentie opgeslagen:
    • Openen kan met rechtermuis op het project > "Edit Project.csproj"
    • Visual Studio opent dan een XML file
  • Deze file kun je ook rechtstreeks bewerken om de versie van een NuGet Package te wijzigen
    • Vergeet niet om de file te bewaren en
    • Om de Solution met Rebuild opnieuw te compileren
  • In de volgende 2 sheets staan correcte voorbeelden van Project.csproj-files
    • soms gebeurt het dat je een Warning blijft zien maar de unittesten met xUnit werken nu wel
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" /> </ItemGroup> </Project>
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version=“2.1.3" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" /> <PackageReference Include="xunit" Version="2.3.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\WebApplicationUnitTest2\WebApplicationUnitTest2.csproj" /> </ItemGroup> </Project>
  • We testen een Action-method (een unit!)
    • We doen geen http-Request, maar roepen de methode rechtstreeks aan
    • Je kunt voor alle parameters een waarde meegeven (normaal bij elke method)
  • Vervolgens test je het antwoord (dat van het type IActionResult is):
    • Is het resultaat een view?
    • Dat kan eventueel ook de view van een ‘404 Not Found’ zijn
    • En je kunt ook testen of een RedirectToAction wordt uitgevoerd.
    • Welke ViewData wordt doorgegeven aan de View?
    • Welk Model wordt doorgegeven aan de View?
    • Zijn deze correct op basis van de input?
  • NB: De resulterende HTML wordt helemaal niet getest!!!

De meeste controllers geven ook een Model mee aan de View, bijvoorbeeld:

public IActionResult Index() { List<Student> studentenLijst = new List<Student>() { new Student() { Id = 1, Name = "Bert"}, new Student() { Id = 5, Name = "Ernie" } }; return View(studentenLijst); }

Hoe test je of in deze Unit het juiste model wordt meegegeven?

[Fact] public void AllStudents_Index_StudentController() { StudentController c = new StudentController(); var result = c.Index(); //result moet een view zijn var viewResult = Assert.IsType<ViewResult>(result); //checken op correcte Model Data: is het de correcte class? var model = Assert.IsAssignableFrom<List<Student>> (viewResult.ViewData.Model); //bevat het de juiste data Assert.Equal(2, model.Count); Assert.Equal("Ernie", model[1].Name); //enz. }
  • Had je ook een View aangemaakt bij de Action-method Index in StudentController?
    • Zo ja: Hernoem deze view
    • Zo nee: Slaagde de unittest?
  • Heb je de Action-Method ook aangeroepen in de browser?
  • En? Werkt deze? Nee, want de browser geeft een lelijke foutmelding:
    • Dit is het beste bewijs dat je de View (die de HTML aanmaakt) echt niet test in je Unittest!

Stel: je hebt op een Home-button geklikt in StudentController

public IActionResult Home(Student s) { if (ModelState.IsValid) // Hier wordt normaal data naar db geschreven. Daarna // overstap van StudentController naar HomeController. return RedirectToAction("Index", "Home"); else return View(s); }

Hoe controleer je nu of Routing correct gebeurt (in dit geval naar HomeController, Action Index)?

[Fact] public void Valid_Redirect_To_HomeController() { StudentController c = new StudentController(); var result = c.Home(null); var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result); Assert.Equal("Home", redirectToActionResult.ControllerName); Assert.Equal("Index", redirectToActionResult.ActionName); }

Nu zonder database, straks met:

[HttpPost] public IActionResult Edit(Student s) { if (ModelState.IsValid) //hier hoort nog eigenlijk wegschrijven naar Db return RedirectToAction("Index"); else return View(s); }

Check of RedirectToAction met valide student wordt uitgevoerd

[Fact] public void Valid_Edit_StudentController() { StudentController c = new StudentController(); Student s = new Student() { Name = "Willem" }; var result = c.Edit(s); var redirectToActionResult = Assert.IsType(result); // Met Assert.Null (…) controleer je of de aanroep binnen dezelfde // Controller wordt uitgevoerd. Assert.Null(redirectToActionResult.ControllerName); Assert.Equal("Index", redirectToActionResult.ActionName); }

N.B.: [HttpPost] is voor het testen irrelevant.

Willem-Alexander heeft een te lange naam (Annotation bij Name is [MaxLength(10)])

[Fact] public void Invalid_Edit_StudentController() { StudentController c = new StudentController(); Student s = new Student() { Name = "Willem-Alexander" }; var result = c.Edit(s); var viewResult = Assert.IsType<ViewResult>(result); Assert.Null(viewResult.ViewName); // als dezelfde naam als de action methode //Checking model enz. //Checking voor juiste foutboodschap bij de Model error }
  • Huh?
  • Helaas, de unittest faalt!
  • Via de foutboodschap maar ook via debuggen kun je zien wat er gebeurt
  • ModelState.isValid == true
  • Huh? Het model bevat toch een te lange naam?
  • Hierdoor kom je niet in de else van het if-statement
  • Het valideren van de methode ModelState.isValid is ook helemaal niet nodig
    • Wie heeft deze methode gemaakt?
    • Deze methode is gemaakt door developers van Microsoft en is vast OK.
    • Dat hoef je dus niet meer te testen
  • Wat wil jij wel unittesten?
    • De code die jij zelf voor je Action-method hebt geschreven
  • Om dat goed te kunnen testen, voeg je zelf een ModelError toe
  • Vervolgens kun je wel checken of jouw Action-method hier correct op reageert

Dus de correcte unittest code is

[Fact] public void Invalid_Edit_StudentController() { StudentController c = new StudentController(); Student s = new Student() { Name = "Willem" }; c.ModelState.AddModelError("Error", "Name too long"); // |== var result = c.Edit(s); var viewResult = Assert.IsType(result); // Met Assert.Null (…) controleer je of de View met dezelfde naam // als de Action-method wordt aangeroepen. Assert.Null(viewResult.ViewName); //Checking model enz. //Checking voor juiste foutboodschap bij de Model error }
Hoe kun je een webapplicatie met database testen (zonder afhankelijk te zijn van de inhoud van die database)?

Lees dit over Mock-objecten.

  • Mock-Objecten zijn een soort nepobjecten die zich net zo gedragen als een echt Object
    • Een Mock-object heeft precies dezelfde interface
  • Vooral bij Unittesten is dit erg handig
  • Wat is het probleem bij een unittest als deze een query maakt naar de database?
    1. Je kunt niet meer spreken van een unittest omdat je test ook afhankelijk is van inhoud en structuur van de database
      1. Je hebt dus een database (met inhoud) nodig om de unittest te kunnen uitvoeren
    2. Je test wordt afhankelijk van de testdata die in de database staat
      1. En wat als een andere unittest data toevoegt of verwijdert?
      2. Of als een tester dit doet tijdens een handmatige test?
  • Installatie dotnet add package Moq en daarna using Moq;
  • public interface ICaptcha { Image image { get; } bool Check(string GebruikerInvoer); } public interface ICaptchaMaker { ICaptcha MaakCaptcha(int complexiteit); } public class LoginController : Controller { public LoginController(ICaptchaMaker captchaMaker) { /* ... */ } }
  • Dit is een soort mocking waarbij je een tijdelijke database gebruikt (i.p.v. bijv. SQL Server)
  • Dit wordt ook uitgelegd op http://gunnarpeipman.com/aspnet-core-ef-inmemory-database/
  • In je unittest geef je een database-context mee die gebruik maakt van een InMemory DB
  • Deze is veel lichter en is geen relationele database
  • Voor iedere unittest maak je de noodzakelijke testdata aan
  • En gooi je die na de test weer weg
  • InMemory DB is geen serieuze vervanging voor een relationele DB, maar zeer geschikt voor tests:
  • Je kunt bijv. data opslaan die normaal gesproken strijdig zouden zijn met de referentiële integriteit in een relationele database
  • Voeg Microsoft.EntityFrameworkCore.InMemory toe aan het project
private ApplicationDbContext GetInMemoryDBMetData() { ApplicationDbContext context = GetNewInMemoryDatabase(); context.Add(new Student { Id = 1, Name = "Jan", Leeftijd = 58 }); context.Add(new Student { Id = 2, Name = "Imane", Leeftijd = 19 }); context.SaveChanges(); return context; } private ApplicationDbContext GetNewInMemoryDatabase() { var options = new DbContextOptionsBuilder<ApplicationDbContext>() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new ApplicationDbContext(options); }

Gebruik een clean context om Loading fouten te vinden.

private string databaseName; // zonder deze property kun je geen clean context maken. private ApplicationDbContext GetInMemoryDBMetData() { ApplicationDbContext context = GetNewInMemoryDatabase(true); context.Add(new Student { Id = 1, Name = "Jan", Leeftijd = 58 }); context.Add(new Student { Id = 2, Name = "Imane", Leeftijd = 19 }); context.SaveChanges(); return GetNewMemoryDatabase(false); // gebruik een nieuw (clean) object voor de context } private ApplicationDbContext GetNewInMemoryDatabase(bool NewDb) { if (NewDb) this.databaseName = Guid.NewGuid().ToString(); // unieke naam var options = new DbContextOptionsBuilder<ApplicationDbContext>() .UseInMemoryDatabase(this.databaseName) .Options; return new ApplicationDbContext(options); }

We gaan verder met de webapplicatie uit week 10b. Deze mag je ook uitbreiden.

  • Sla de studenten nu op in een SQLite database
  • Injecteer de DBContext in de controller(s)
  • Maak gebruik van een InMemory Database met 5 personen.
  • Schrijf 5 unit tests.
  • Als je dat nodig vindt kun je voor het testen van aangeroepen methodes eventueel Moq gebruiken.