Delen via


ASP.NET Core MVC-apps testen

Tip

Deze inhoud is een fragment uit het eBook, Architect Modern Web Applications met ASP.NET Core en Azure, beschikbaar op .NET Docs of als een gratis downloadbare PDF die offline kan worden gelezen.

Architect Modern Web Applications with ASP.NET Core and Azure eBook cover thumbnail.

"Als u niet tevreden bent over het testen van uw product, zullen uw klanten het waarschijnlijk ook niet graag testen." _-Anonieme-

Software van elke complexiteit kan op onverwachte manieren mislukken als reactie op wijzigingen. Testen na het aanbrengen van wijzigingen is dus vereist voor alle, maar voor de meest triviale (of minst kritieke) toepassingen. Handmatig testen is de traagste, minst betrouwbare, duurste manier om software te testen. Als toepassingen niet zijn ontworpen om te testen, kan het helaas de enige manier zijn om te testen. Toepassingen die zijn geschreven om de in hoofdstuk 4 beschreven architectuurprincipes te volgen, moeten grotendeels eenheidstestbaar zijn. ASP.NET Core-toepassingen ondersteunen geautomatiseerde integratie en functionele tests.

Soorten geautomatiseerde tests

Er zijn veel soorten geautomatiseerde tests voor softwaretoepassingen. De eenvoudigste, laagste niveautest is de eenheidstest. Op iets hoger niveau zijn er integratietests en functionele tests. Andere soorten tests, zoals UI-tests, belastingstests, stresstests en betrouwbaarheidstests, vallen buiten het bereik van dit document.

Unittests

Met een eenheidstest wordt één deel van de logica van uw toepassing getest. U kunt het verder beschrijven door een aantal van de dingen te vermelden die het niet is. Met een eenheidstest wordt niet getest hoe uw code werkt met afhankelijkheden of infrastructuur. Dit is waar integratietests voor zijn. Met een eenheidstest wordt het framework waarop uw code is geschreven niet getest. U moet ervan uitgaan dat deze werkt of, als u dit niet vindt, een bug opneemt en een tijdelijke oplossing codet. Een eenheidstest wordt volledig uitgevoerd in het geheugen en in het proces. Het communiceert niet met het bestandssysteem, het netwerk of een database. Eenheidstests mogen alleen uw code testen.

Eenheidstests, omdat ze slechts één eenheid van uw code testen, zonder externe afhankelijkheden, moeten zeer snel worden uitgevoerd. Daarom moet u in een paar seconden testsuites van honderden eenheidstests kunnen uitvoeren. Voer ze regelmatig uit, idealiter vóór elke push naar een gedeelde opslagplaats voor broncodebeheer, en zeker met elke geautomatiseerde build op uw buildserver.

Integratietests

Hoewel het een goed idee is om uw code in te kapselen die communiceert met infrastructuur zoals databases en bestandssystemen, hebt u nog steeds een deel van die code en wilt u deze waarschijnlijk testen. Daarnaast moet u controleren of de lagen van uw code werken zoals verwacht wanneer de afhankelijkheden van uw toepassing volledig zijn opgelost. Deze functionaliteit is de verantwoordelijkheid van integratietests. Integratietests zijn meestal langzamer en moeilijker in te stellen dan eenheidstests, omdat ze vaak afhankelijk zijn van externe afhankelijkheden en infrastructuur. U moet dus voorkomen dat u dingen test die kunnen worden getest met eenheidstests in integratietests. Als u een bepaald scenario met een eenheidstest kunt testen, moet u dit testen met een eenheidstest. Als u dat niet kunt, kunt u overwegen een integratietest te gebruiken.

Integratietests hebben vaak complexere installatie- en scheurprocedures dan eenheidstests. Een integratietest die wordt uitgevoerd op een werkelijke database, heeft bijvoorbeeld een manier nodig om de database terug te keren naar een bekende status voordat elke test wordt uitgevoerd. Naarmate er nieuwe tests worden toegevoegd en het productiedatabaseschema zich verder ontwikkelt, worden deze testscripts meestal groter en complexer. In veel grote systemen is het niet praktisch om volledige suites van integratietests uit te voeren op ontwikkelwerkstations voordat wijzigingen in gedeeld broncodebeheer worden gecontroleerd. In dergelijke gevallen kunnen integratietests worden uitgevoerd op een buildserver.

Functionele tests

Integratietests worden vanuit het perspectief van de ontwikkelaar geschreven om te controleren of sommige onderdelen van het systeem correct samenwerken. Functionele tests worden vanuit het perspectief van de gebruiker geschreven en controleren de juistheid van het systeem op basis van de vereisten. Het volgende fragment biedt een nuttige analogie voor het nadenken over functionele tests, vergeleken met eenheidstests:

"Vaak is de ontwikkeling van een systeem vergelijkbaar met het gebouw van een huis. Hoewel deze analogie niet helemaal juist is, kunnen we deze uitbreiden om het verschil tussen eenheids- en functionele tests te begrijpen. Eenheidstests zijn vergelijkbaar met een bouwinspecteur die de bouwplaats van een huis bezoekt. Hij richt zich op de verschillende interne systemen van het huis, de basis, framen, elektrische, loodgieters, enzovoort. Hij zorgt ervoor dat de onderdelen van het huis correct en veilig werken, dat wil zeggen, voldoen aan de bouwcode. Functionele tests in dit scenario zijn vergelijkbaar met de huiseigenaar die dezelfde bouwplaats bezoekt. Hij gaat ervan uit dat de interne systemen zich correct gedragen, dat de bouwinspecteur zijn taak uitvoert. De huiseigenaar is gericht op hoe het zal zijn om in dit huis te wonen. Hij is bezorgd over hoe het huis eruitziet, zijn de verschillende kamers een comfortabele grootte, past het huis aan de behoeften van de familie, zijn de ramen op een goede plek om de ochtendzon te vangen. De huiseigenaar voert functionele tests uit op het huis. Hij heeft het perspectief van de gebruiker. De bouwinspecteur voert eenheidstests uit op het huis. Hij heeft het perspectief van de bouwer."

Bron: Eenheidstests versus functionele tests

Ik vind het leuk om te zeggen dat we als ontwikkelaars op twee manieren falen: we bouwen het fout of we bouwen het verkeerde. Eenheidstests zorgen ervoor dat u het juiste bouwt; functionele tests zorgen ervoor dat u het juiste bouwt.

Omdat functionele tests op systeemniveau worden uitgevoerd, kunnen ze enige mate van UI-automatisering vereisen. Net als integratietests werken ze meestal ook met een soort testinfrastructuur. Deze activiteit maakt ze langzamer en meer broos dan eenheids- en integratietests. U moet slechts zoveel functionele tests hebben als u er zeker van moet zijn dat het systeem zich gedraagt zoals gebruikers verwachten.

Piramide testen

Martin Fowler schreef over de test piramide, waarvan een voorbeeld wordt weergegeven in afbeelding 9-1.

Testing Pyramid

Afbeelding 9-1. Piramide testen

De verschillende lagen van de piramide en hun relatieve grootten vertegenwoordigen verschillende soorten tests en hoeveel u moet schrijven voor uw toepassing. Zoals u kunt zien, is het raadzaam om een grote basis van eenheidstests te hebben, ondersteund door een kleinere laag integratietests, met een nog kleinere laag functionele tests. Elke laag moet in het ideale geval alleen tests bevatten die niet adequaat op een lagere laag kunnen worden uitgevoerd. Houd rekening met de test piramide wanneer u probeert te bepalen welk type test u nodig hebt voor een bepaald scenario.

Wat u moet testen

Een veelvoorkomend probleem voor ontwikkelaars die onervaren zijn met het schrijven van geautomatiseerde tests, komen overeen met wat er moet worden getest. Een goed uitgangspunt is het testen van voorwaardelijke logica. Overal waar u een methode hebt met gedrag dat verandert op basis van een voorwaardelijke instructie (if-else, switch, enzovoort), moet u ten minste een aantal tests kunnen bedenken die het juiste gedrag voor bepaalde voorwaarden bevestigen. Als uw code foutvoorwaarden heeft, is het verstandig om ten minste één test te schrijven voor het 'gelukkige pad' via de code (zonder fouten) en ten minste één test voor het 'droevige pad' (met fouten of atypische resultaten) om te bevestigen dat uw toepassing zich gedraagt zoals verwacht bij fouten. Probeer ten slotte te focussen op het testen van dingen die kunnen mislukken, in plaats van te focussen op metrische gegevens, zoals codedekking. Meer codedekking is beter dan minder, over het algemeen. Het schrijven van een aantal tests van een complexe en bedrijfskritieke methode is meestal een beter gebruik van tijd dan het schrijven van tests voor auto-eigenschappen om de metrische gegevens van de codedekking te verbeteren.

Testprojecten organiseren

Testprojecten kunnen echter het beste voor u worden georganiseerd. Het is een goed idee om tests te scheiden per type (eenheidstest, integratietest) en door wat ze testen (per project, op naamruimte). Of deze scheiding nu bestaat uit mappen binnen één testproject of meerdere testprojecten, is een ontwerpbeslissing. Een project is eenvoudigst, maar voor grote projecten met veel tests of om gemakkelijker verschillende sets tests uit te voeren, wilt u mogelijk verschillende testprojecten hebben. Veel teams organiseren testprojecten op basis van het project dat ze testen, wat voor toepassingen met meer dan een paar projecten kan leiden tot een groot aantal testprojecten, met name als u deze nog steeds opsplitst op basis van het soort tests in elk project. Een compromisbenadering bestaat uit één project per soort test, per toepassing, met mappen in de testprojecten om aan te geven dat het project (en de klasse) wordt getest.

Een veelvoorkomende aanpak is het organiseren van de toepassingsprojecten onder een 'src'-map en de testprojecten van de toepassing onder een parallelle map 'tests'. U kunt overeenkomende oplossingsmappen maken in Visual Studio als u deze organisatie nuttig vindt.

Test organization in your solution

Afbeelding 9-2. Organisatie testen in uw oplossing

U kunt het testframework gebruiken dat u wilt gebruiken. Het xUnit-framework werkt goed en is waar alle ASP.NET Core- en EF Core-tests in worden geschreven. U kunt een xUnit-testproject toevoegen in Visual Studio met behulp van de sjabloon die wordt weergegeven in afbeelding 9-3 of vanuit de CLI.dotnet new xunit

Add an xUnit Test Project in Visual Studio

Afbeelding 9-3. Een xUnit-testproject toevoegen in Visual Studio

Naamgeving testen

Geef uw tests een consistente naam, met namen die aangeven wat elke test doet. Eén benadering waarmee ik veel succes heb gehad, is het benoemen van testklassen op basis van de klasse en methode die ze testen. Deze aanpak resulteert in veel kleine testklassen, maar het maakt het uiterst duidelijk waarvoor elke test verantwoordelijk is. Wanneer de naam van de testklasse is ingesteld, kan de naam van de testmethode worden gebruikt om het geteste gedrag op te geven. Deze naam moet het verwachte gedrag en eventuele invoer of veronderstellingen bevatten die dit gedrag moeten opleveren. Enkele voorbeeldtestnamen:

  • CatalogControllerGetImage.CallsImageServiceWithId

  • CatalogControllerGetImage.LogsWarningGivenImageMissingException

  • CatalogControllerGetImage.ReturnsFileResultWithBytesGivenSuccess

  • CatalogControllerGetImage.ReturnsNotFoundResultGivenImageMissingException

Een variant van deze benadering beëindigt elke testklassenaam met 'Should' en wijzigt de gespannenheid enigszins:

  • CatalogControllerGetImageMoet.worden aangeroepenImageServiceWithId

  • CatalogControllerGetImageMoet.worden vastgelegdWarningGivenImageMissingException

Sommige teams vinden de tweede naamgevingsbenadering duidelijker, maar iets uitgebreider. Probeer in elk geval een naamconventie te gebruiken die inzicht biedt in testgedrag, zodat wanneer een of meer tests mislukken, het duidelijk is uit hun namen welke gevallen zijn mislukt. Vermijd het benoemen van uw tests vaag, zoals ControllerTests.Test1, omdat deze namen geen waarde bieden wanneer u ze in testresultaten ziet.

Als u een naamconventie volgt zoals hierboven die veel kleine testklassen produceert, is het een goed idee om uw tests verder te organiseren met behulp van mappen en naamruimten. Afbeelding 9-4 toont één benadering voor het ordenen van tests per map binnen verschillende testprojecten.

Organizing test classes by folder based on class being tested

Afbeelding 9-4. Testklassen organiseren op map op basis van de testklasse.

Als een bepaalde toepassingsklasse veel methoden heeft die worden getest (en dus veel testklassen), kan het zinvol zijn om deze klassen in een map te plaatsen die overeenkomt met de toepassingsklasse. Deze organisatie verschilt niet van de manier waarop u bestanden ergens anders in mappen kunt ordenen. Als u meer dan drie of vier gerelateerde bestanden in een map met veel andere bestanden hebt, is het vaak handig om ze naar hun eigen submap te verplaatsen.

Eenheidstests ASP.NET Core-apps

In een goed ontworpen ASP.NET Core-toepassing worden de meeste complexiteit en bedrijfslogica ingekapseld in bedrijfsentiteiten en diverse services. De ASP.NET Core MVC-app zelf, met de bijbehorende controllers, filters, viewmodels en weergaven, moeten slechts enkele eenheidstests vereisen. Veel van de functionaliteit van een bepaalde actie ligt buiten de actiemethode zelf. Testen of routering of globale foutafhandeling correct werkt, kan niet effectief worden uitgevoerd met een eenheidstest. Op dezelfde manier kunnen filters, waaronder modelvalidatie en verificatie- en autorisatiefilters, niet worden getest met een test die is gericht op de actiemethode van een controller. Zonder deze gedragsbronnen moeten de meeste actiemethoden triviaal klein zijn, waarbij het grootste deel van hun werk wordt gedelegeerd aan services die kunnen worden getest onafhankelijk van de controller die ze gebruikt.

Soms moet u uw code herstructureren om deze te testen. Deze activiteit omvat vaak het identificeren van abstracties en het gebruik van afhankelijkheidsinjectie om toegang te krijgen tot de abstractie in de code die u wilt testen, in plaats van rechtstreeks te coderen op basis van infrastructuur. Denk bijvoorbeeld aan deze eenvoudige actiemethode voor het weergeven van afbeeldingen:

[HttpGet("[controller]/pic/{id}")]
public IActionResult GetImage(int id)
{
  var contentRoot = _env.ContentRootPath + "//Pics";
  var path = Path.Combine(contentRoot, id + ".png");
  Byte[] b = System.IO.File.ReadAllBytes(path);
  return File(b, "image/png");
}

Het testen van deze methode wordt moeilijk gemaakt door de directe afhankelijkheid ervan System.IO.File, die wordt gebruikt om uit het bestandssysteem te lezen. U kunt dit gedrag testen om ervoor te zorgen dat het werkt zoals verwacht, maar dit met echte bestanden is een integratietest. Het is de moeite waard om te noteren dat u de route van deze methode niet kunt testen. U ziet hoe u deze test kunt uitvoeren met een functionele test.

Als u het gedrag van het bestandssysteem niet rechtstreeks kunt testen en u de route niet kunt testen, wat is er om te testen? Na het herstructureren om eenheidstests mogelijk te maken, kunt u enkele testcases en ontbrekend gedrag ontdekken, zoals foutafhandeling. Wat doet de methode wanneer een bestand niet wordt gevonden? Wat moet het doen? In dit voorbeeld ziet de geherstructureerde methode er als volgt uit:

[HttpGet("[controller]/pic/{id}")]
public IActionResult GetImage(int id)
{
  byte[] imageBytes;
  try
  {
    imageBytes = _imageService.GetImageBytesById(id);
  }
  catch (CatalogImageMissingException ex)
  {
    _logger.LogWarning($"No image found for id: {id}");
    return NotFound();
  }
  return File(imageBytes, "image/png");
}

_logger en _imageService worden beide geïnjecteerd als afhankelijkheden. U kunt nu testen of dezelfde id die wordt doorgegeven aan de actiemethode, wordt doorgegeven aan _imageServiceen dat de resulterende bytes worden geretourneerd als onderdeel van FileResult. U kunt ook testen of de logboekregistratie van fouten op de verwachte wijze plaatsvindt en dat er een NotFound resultaat wordt geretourneerd als de afbeelding ontbreekt, ervan uitgaande dat dit gedrag belangrijk gedrag van de toepassing is (dus niet alleen tijdelijke code die de ontwikkelaar heeft toegevoegd om een probleem vast te stellen). De werkelijke bestandslogica is verplaatst naar een afzonderlijke implementatieservice en is uitgebreid om een toepassingsspecifieke uitzondering te retourneren voor het geval van een ontbrekend bestand. U kunt deze implementatie onafhankelijk testen met behulp van een integratietest.

In de meeste gevallen wilt u globale uitzonderingshandlers in uw controllers gebruiken, dus de hoeveelheid logica in deze moet minimaal zijn en waarschijnlijk niet de moeite waard zijn om eenheidstests uit te voeren. Voer de meeste tests van controlleracties uit met behulp van functionele tests en de TestServer klasse die hieronder wordt beschreven.

Integratietests ASP.NET Core-apps

De meeste integratietests in uw ASP.NET Core-apps moeten services en andere implementatietypen testen die zijn gedefinieerd in uw infrastructuurproject. U kunt bijvoorbeeld testen of EF Core de gegevens heeft bijgewerkt en opgehaald die u verwacht van uw gegevenstoegangsklassen die zich in het infrastructuurproject bevond. De beste manier om te testen of uw ASP.NET Core MVC-project correct werkt, is met functionele tests die worden uitgevoerd op uw app die wordt uitgevoerd in een testhost.

Functionele tests ASP.NET Core-apps

Voor ASP.NET Core-toepassingen maakt de TestServer klasse functionele tests vrij eenvoudig te schrijven. U configureert een TestServer rechtstreeks (of) met behulp van een WebHostBuilder (of HostBuilder) (zoals u normaal doet voor uw toepassing) of met het WebApplicationFactory type (beschikbaar sinds versie 2.1). Probeer uw testhost zo goed mogelijk aan uw productiehost te koppelen, zodat uw testoefeningen vergelijkbaar zijn met wat de app in productie zal doen. De WebApplicationFactory klasse is handig voor het configureren van de ContentRoot van de TestServer, die wordt gebruikt door ASP.NET Core om statische resources, zoals weergaven, te vinden.

U kunt eenvoudige functionele tests maken door een testklasse te maken die implementeert IClassFixture<WebApplicationFactory<TEntryPoint>>, waar TEntryPoint is de klasse van Startup uw webtoepassing. Met deze interface kan uw testarmatur een client maken met behulp van de methode van CreateClient de fabriek:

public class BasicWebTests : IClassFixture<WebApplicationFactory<Program>>
{
  protected readonly HttpClient _client;

  public BasicWebTests(WebApplicationFactory<Program> factory)
  {
    _client = factory.CreateClient();
  }

  // write tests that use _client
}

Tip

Als u een minimale API-configuratie gebruikt in uw Program.cs-bestand , wordt de klasse standaard intern gedeclareerd en is deze niet toegankelijk vanuit het testproject. U kunt in plaats daarvan elke andere instantieklasse in uw webproject kiezen of deze toevoegen aan uw Program.cs-bestand :

// Make the implicit Program class public so test projects can access it
public partial class Program { }

Vaak wilt u een extra configuratie van uw site uitvoeren voordat elke test wordt uitgevoerd, zoals het configureren van de toepassing voor het gebruik van een in-memory gegevensarchief en vervolgens het seeden van de toepassing met testgegevens. Als u deze functionaliteit wilt bereiken, maakt u uw eigen subklasse van WebApplicationFactory<TEntryPoint> en overschrijft u de ConfigureWebHost bijbehorende methode. Het onderstaande voorbeeld is afkomstig uit het project eShopOnWeb FunctionalTests en wordt gebruikt als onderdeel van de tests op de hoofdwebtoepassing.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;

namespace Microsoft.eShopWeb.FunctionalTests.Web;
public class WebTestFixture : WebApplicationFactory<Startup>
{
  protected override void ConfigureWebHost(IWebHostBuilder builder)
  {
    builder.UseEnvironment("Testing");

    builder.ConfigureServices(services =>
    {
      services.AddEntityFrameworkInMemoryDatabase();

      // Create a new service provider.
      var provider = services
            .AddEntityFrameworkInMemoryDatabase()
            .BuildServiceProvider();

      // Add a database context (ApplicationDbContext) using an in-memory
      // database for testing.
      services.AddDbContext<CatalogContext>(options =>
      {
        options.UseInMemoryDatabase("InMemoryDbForTesting");
        options.UseInternalServiceProvider(provider);
      });

      services.AddDbContext<AppIdentityDbContext>(options =>
      {
        options.UseInMemoryDatabase("Identity");
        options.UseInternalServiceProvider(provider);
      });

      // Build the service provider.
      var sp = services.BuildServiceProvider();

      // Create a scope to obtain a reference to the database
      // context (ApplicationDbContext).
      using (var scope = sp.CreateScope())
      {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<CatalogContext>();
        var loggerFactory = scopedServices.GetRequiredService<ILoggerFactory>();

        var logger = scopedServices
            .GetRequiredService<ILogger<WebTestFixture>>();

        // Ensure the database is created.
        db.Database.EnsureCreated();

        try
        {
          // Seed the database with test data.
          CatalogContextSeed.SeedAsync(db, loggerFactory).Wait();

          // seed sample user data
          var userManager = scopedServices.GetRequiredService<UserManager<ApplicationUser>>();
          var roleManager = scopedServices.GetRequiredService<RoleManager<IdentityRole>>();
          AppIdentityDbContextSeed.SeedAsync(userManager, roleManager).Wait();
        }
        catch (Exception ex)
        {
          logger.LogError(ex, $"An error occurred seeding the " +
                    "database with test messages. Error: {ex.Message}");
        }
      }
    });
  }
}

Tests kunnen gebruikmaken van deze aangepaste WebApplicationFactory door deze te gebruiken om een client te maken en vervolgens aanvragen te doen voor de toepassing met behulp van dit clientexemplaren. De toepassing bevat gegevens die kunnen worden gebruikt als onderdeel van de asserties van de test. Met de volgende test wordt gecontroleerd of de startpagina van de eShopOnWeb-toepassing correct wordt geladen en een productvermelding bevat die is toegevoegd aan de toepassing als onderdeel van de seed-gegevens.

using Microsoft.eShopWeb.FunctionalTests.Web;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.eShopWeb.FunctionalTests.WebRazorPages;
[Collection("Sequential")]
public class HomePageOnGet : IClassFixture<WebTestFixture>
{
  public HomePageOnGet(WebTestFixture factory)
  {
    Client = factory.CreateClient();
  }

  public HttpClient Client { get; }

  [Fact]
  public async Task ReturnsHomePageWithProductListing()
  {
    // Arrange & Act
    var response = await Client.GetAsync("/");
    response.EnsureSuccessStatusCode();
    var stringResponse = await response.Content.ReadAsStringAsync();

    // Assert
    Assert.Contains(".NET Bot Black Sweatshirt", stringResponse);
  }
}

Deze functionele test oefent de volledige ASP.NET Core MVC/Razor Pages-toepassingsstack, inclusief alle middleware, filters en binders die mogelijk aanwezig zijn. Er wordt gecontroleerd of een bepaalde route (/) de verwachte successtatuscode en HTML-uitvoer retourneert. Dit doet u zonder een echte webserver in te stellen en vermijdt veel van de broosheid die het gebruik van een echte webserver voor testen kan ervaren (bijvoorbeeld problemen met firewallinstellingen). Functionele tests die worden uitgevoerd op TestServer zijn meestal trager dan integratie- en eenheidstests, maar zijn veel sneller dan tests die via het netwerk naar een testwebserver zouden worden uitgevoerd. Gebruik functionele tests om ervoor te zorgen dat de front-endstack van uw toepassing werkt zoals verwacht. Deze tests zijn vooral handig wanneer u duplicatie in uw controllers of pagina's vindt en u de duplicatie adresseren door filters toe te voegen. In het ideale geval verandert deze herstructurering het gedrag van de toepassing niet en een suite met functionele tests controleert of dit het geval is.

Verwijzingen – Test ASP.NET Core MVC-apps