Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Web-based Applications with .NET Core

Web-based Applications with .NET Core

Pawel Gerr

June 29, 2017
Tweet

More Decks by Pawel Gerr

Other Decks in Programming

Transcript

  1. • .NET Standard • .NET Core • Dependency Injection •

    Configuration • Logging • ASP.NET Core • Kestrel • Request Pipeline • Startup / Shutdown • Entity Framework Core • Code-first / Database-first • Migrations Agenda
  2. vs .NET Core • Specification of .NET APIs • .NET

    Core is a cross-platform runtime: Windows, Linux, MacOS • Versions: 1.0 – 1.6 (v2.0 in Q3 2017) • Runtime support (list is not complete) • .NET Core 1.0 ➡ 1.0 – 1.6 • .NET Core 2.0 ➡ 1.0 – 2.0 • .NET 4.5 ➡ 1.0 – 1.1 • .NET 4.6 ➡ 1.0 – 1.3 • .NET 4.6.1 ➡ 1.0 – 2.0 • Open Source: https://github.com/dotnet/core .NET Standard 2.0 1.6 1.2 1.0
  3. vs Portable Class Library (PCL) PCL (platform specific) .NET Standard

    (platform agnostic) .NET Standard 2.0 1.6 1.2 1.0 Platform A Platform B Platform C
  4. Lower version for libraries Bigger version ➡ bigger capabilities ➡

    worse platform support Lower version ➡ less capabilities ➡ better platform support .NET Standard
  5. Multi-Target Libraries • Provide different APIs via conditional compilation •

    System.Runtime supports • .NETFramework 4.5, 4.6.2 • .NETStandard 1.0, 1.2, 1.3, 1.5 • .... • Can have different dependencies • System.Runtime dependencies • .NETFramework 4.5, 4.6.2 ➡ No dependencies • .NETStandard 1.0 ➡ Microsoft.NETCore.Platforms, ... .NET Standard IntelliSense for type TypeCode #if NETSTANDARD1_3 || NETSTANDARD1_4 var code = TypeCode.Boolean; #endif
  6. Platform-dependent code • Use RuntimeInformation to determine the platform .NET

    Standard if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { ... } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { ... } else { ... }
  7. Tooling changes • Requires Visual Studio 2017, Visual Studio Code

    or JetBrains Rider • .NET Core is very modular ➡ everthing is a nuget package • Implicit references on meta packages: NetStandard.Library and Microsoft.NETCore.App • Standard files (like .cs) are included into project per default • Nuget v4: • Project/Nuget package references as well as nuspec metadata are in *.csproj • Transitive package restore • Assembly info can be defined in *.csproj as well instead of in AssemblyInfo.cs • CLI: dotnet restore/build/pack/publish .NET Standard
  8. Drawbacks • Visual Studio 2017 is slower in comparison when

    working with full-blown .NET Framework like .NET 4.6 • ReSharper slows Visual Studio even more • Manual adjustments of project files (*.csproj) are required for some features (e.g. multi-targeting) • Application consisting of 300+ assemblies is easy to reach • Nuget (v3) can be very slow when using „old“ tooling with .NET Standard compatible packages • A few nuget packages „forget“ to copy necessery assemblies to output directory on build • Search for package containing specific type (http://packagesearch.azurewebsites.net/, https://apisof.net/) • Some .NET members (properties, methods) throw PlatformNotSupportedException .NET Standard
  9. Dependency Injection (DI) • IServiceCollection will be provided by ASP.NET

    Core • Otherwise create with new ServiceCollection() • Recommendation: use your favorite DI framework Issue when working with objects implementing IDisposable: http://weblogs.thinktecture.com/pawel/2017/02/aspnet-core-dependecy-injection-disposing.html .NET Core serviceCollection.AddSingleton<IMyComponent, MyComponent>(); // Alternative registrations serviceCollection.AddSingleton<IMyComponent>(provider => new MyComponent()); serviceCollection.AddScoped<IMyComponent, MyComponent>(); serviceCollection.AddTransient<IMyComponent, MyComponent>(); // Creates DI container IServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); // Resolve types IMyComponent myComponent = serviceProvider.GetRequiredService<IMyComponent>(); 1
  10. Configuration • Key-value pair based configuration • Usage .NET Core

    IConfiguration config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary<string, string>() { {"Key", "Value"} }) // NuGet package: Microsoft.Extensions.Configuration.EnvironmentVariables .AddEnvironmentVariables() // NuGet package: Microsoft.Extensions.Configuration.CommandLine .AddCommandLine(args) // NuGet package: Microsoft.Extensions.Configuration.Json .AddJsonFile("configuration.json", optional:true, reloadOnChange:true) ... .Build(); string value = config["Key"]; // string value = config.GetSection("Key").Value; 2
  11. Configuration – Strongly Typed • Strongly typed way of accessing

    settings • MyConfiguration will be injected by the DI .NET Core IServiceCollection serviceCollection; IConfiguration config; // Registers necessary types like IOptions<> serviceCollection.AddOptions(); // Registers „MyConfiguration“ serviceCollection.Configure<MyConfiguration>(config.GetSection("my:configuration")); // Optional serviceCollection.AddSingleton(provider => provider.GetRequiredService<IOptions<MyConfiguration>>().Value); public class MyConfiguration { public int Value { get; set; } } public class MyComponent { public MyComponent(IOptions<MyConfiguration> myConfigWrapper /* MyConfiguration myConfig*/) { } } 3 { "my": { "config": { "value": 42 } } }
  12. Configuration – User Secrets • For sensitive data like API-keys,

    passwords, connection strings, etc. during development • Internal functionality: • Loads the content of a JSON-file from user profile • The file path is derived from the provided User-Secret-Id "MySecretId" https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets .NET Core var configBuilder = new ConfigurationBuilder(); ... configBuilder.AddUserSecrets("MySecretId");
  13. Configuration – Alternative • Thinktecture.Configuration (https://github.com/PawelGerr/Thinktecture.Configuration) • Strongly Typed •

    Support for abstract types (e.g. for inner properties) • DI Support (non-default constructor) • Supports „overrides“ • ... .NET Core var builder = new Autofac.ContainerBuilder(); ... builder.RegisterJsonFileConfigurationProvider("./configuration.json"); builder.RegisterJsonFileConfiguration<MyConfiguration>("My.Config") .As<IMyConfiguration>(); { "my": { "config": { "value": 42 } } } public interface IMyConfiguration { int Value { get; } } public class MyConfiguration : IMyConfiguration { public int Value { get; set; } public MyConfiguration(IMyService service) { ... } }
  14. Logging • Logging library from Microsoft • Output .NET Core

    ILoggerFactory loggerFactory = new LoggerFactory() // NuGet package: Microsoft.Extensions.Logging.Console .AddConsole(LogLevel.Trace, includeScopes: true); ILogger logger = loggerFactory.CreateLogger("MyLogger"); using (logger.BeginScope("My scope")) { logger.LogInformation(eventId: 42, message: "My log message"); } info: MyLogger[42] => My scope My log message 4
  15. Logging with Serilog • Use extension method AddSerilog • Console

    Output .NET Core [16:51:32 INF] [MyLogger - { Id: 42 }] ["My scope"] => My log message Logger serilogLogger = new LoggerConfiguration() .WriteTo.LiterateConsole(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext} - {EventId}] {Scope} => {Message}{NewLine}{Exception}") .CreateLogger(); ILoggerFactory loggerFactory = new LoggerFactory() .AddSerilog(serilogLogger); ILogger logger = loggerFactory.CreateLogger("MyLogger"); using (logger.BeginScope("My scope")) { logger.LogInformation(eventId: 42, message: "My log message"); }
  16. Parts • Web servers • Kestrel: cross-platform • WebListener: Windows

    only • No separation between MVC and Web API • MVC and Web API can be self-hosted now • Build-in dependency injection, configuration and logging infrastructure • Missing pieces • SignalR (in development) • Web Forms (no plans to port it to .NET Core) ASP.NET Core
  17. Web server Kestrel • Cross-platform • HTTPS support • Not

    as feature rich as e.g. IIS but ... • IIS/Nginx/Apache can be put in front of Kestrel (recommended when using on the internet) Setup Issues: http://weblogs.thinktecture.com/pawel/2017/02/aspnet-core-with-iis-setup-issues.html ASP.NET Core HTTPS IIS as reverse proxy Kestrel Internet HTTP
  18. Kestrel Setup • Minimalistic setup • Console Output ASP.NET Core

    static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .Configure(app => { app.Run(async context => await context.Response.WriteAsync("Demo")); }) .Build(); host.Run(); } Hosting environment: Production Content root path: C:\Sources\KestrelSetup\bin\Debug\netcoreapp1.1 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down. 5
  19. Environments • By convention: Development, Staging, Production (default) • Can

    be set via code or environment variable • via command line • via code • Determine the environment by injecting IHostingEnvironment ASP.NET Core set ASPNETCORE_ENVIRONMENT=Development var host = new WebHostBuilder() .UseKestrel() .UseEnvironment("Development") ... // IHostingEnvironment env if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
  20. Startup class • Configuration of the ASP.NET pipeline in Startup

    • Optional: Startup can implement the interface IStartup ASP.NET Core var host = new WebHostBuilder() ... .UseStartup<Startup>() .Build(); public class Startup { public void ConfigureServices(IServiceCollection services) { // configuration of the DI } public void Configure(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Demo"); }); } } 6
  21. DI with 3rd party library (Autofac) – The easy way

    ☺ • Register Autofac with WebHostBuilder • The method ConfigureContainer will be called right after ConfigureServices ASP.NET Core var host = new WebHostBuilder() .ConfigureServices(services => services.AddAutofac()) ... public void ConfigureServices(IServiceCollection services) { // DI configuration } public void ConfigureContainer(ContainerBuilder builder) { // DI configuration } public void Configure(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Demo"); }); }
  22. • Web Application = Web API/MVC + More • More

    control over startup and shutdown of the web server • Multi-step setup: logging ➡ configuration ➡ misc preparations ➡ web server • Graceful shutdown: shutdown of endpoints ➡ processing of outstanding data • Execution of (periodic) tasks DI - More control over the application life cycle ASP.NET Core ContainerBuilder containerBuilder = GetContainerBuilder(); using (var container = containerBuilder.Build()) { var myConfig = container.Resolve<MyConfiguration>(); ... using (var scope = container.BeginLifetimeScope( builder => AdditionalDeps(builder, myConfig))) { ... StartWebServer(scope); ... } } private void StartWebServer(ILifetimeScope scope) { var host = new WebHostBuilder().UseKestrel() .ConfigureServices(s => { s.AddTransient(provider => scope.Resolve<Startup>()); }) .UseStartup<Startup>() .Build(); host.Run(); } 7
  23. DI - More control over the application (Startup) • Most

    ASP.NET libraries offer extension methods for IServiceCollection only • The web server uses a child scope for resolution of dependencies (like controllers) • Requires PR: https://github.com/autofac/ Autofac.Extensions.DependencyInjection/ pull/14 or fork: https://github.com/PawelGerr/ Autofac.Extensions.DependencyInjection or the ASP.NET components must be registered with the root lifetime scope (i.e. the IContainer itself) ASP.NET Core public class Startup { private const string _WEBSERVER_SCOPE_NAME = "ASPNET_WEBSERVER_ROOT"; public Startup(ILifetimeScope scope) { _scope = scope; } public IServiceProvider ConfigureServices(IServiceCollection services) { // register ASP.NET Core components _aspnetScope = _scope.BeginLifetimeScope(_WEBSERVER_SCOPE_NAME, builder => builder.Populate(services, _WEBSERVER_SCOPE_NAME)); return new AutofacServiceProvider(_aspnetScope); } public void Configure(IApplicationBuilder app, IApplicationLifetime appLifetime) { appLifetime.ApplicationStopped.Register(() => _aspnetScope?.Dispose()); ... } }
  24. • AddMvc adds MVC (incl. Razor Engine) and Web API

    components to DI • UseMvc adds MVC and Web API into request pipeline Performance tips: http://weblogs.thinktecture.com/pawel/2017/03/aspnet-core-webapi-performance.html Adding MVC and Web API ASP.NET Core public class Startup { public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); ... } public void Configure(IApplicationBuilder app) { app.UseMvc(); } } [Route("[controller]")] public class DemoController : Controller { [HttpGet("GetTime")] public IActionResult GetTime() { return Json(DateTime.Now); } } http://localhost:5000/demo/gettime 8
  25. MVC • Razor support • Dependency injection in views •

    Use this feature with caution • Complex business logic and database access does not belong into a view ASP.NET Core [Route("[controller]")] public class DemoController : Controller { [HttpGet("Index")] public IActionResult Index() { return View(); } } <!DOCTYPE html> <html> <head> <title>MVC</title> </head> <body> Date: @DateTime.Now </body> </html>
  26. Request Pipeline • Configure pipeline by adding middleware components ASP.NET

    Core public void Configure(IApplicationBuilder app) { app.UseMiddleware<MyMiddleware>(); app.UseStaticFiles(); app.UseMvc(); } MyMiddleware StaticFiles Middleware MVC Middleware Request Response Client Request Pipeline
  27. Logs Correlation – Request Id • ASP.NET Core provides a

    RequestId • Can be get and set via HttpContext.TraceIdentifier • Alternative: change the ASP.NET Core feature responsible for generation of the id ASP.NET Core var serilogLoggerConfig = new LoggerConfiguration() .WriteTo.LiterateConsole(outputTemplate: "... {RequestId} ...") ...; public class MyHttpRequestIdentifierFeatureMiddleware { public MyHttpRequestIdentifierFeatureMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext ctx) { ctx.Features.Set<IHttpRequestIdentifierFeature>(new MyHttpRequestIdentifierFeature()); await _next(ctx); } } public class MyHttpRequestIdentifierFeature : IHttpRequestIdentifierFeature { public string TraceIdentifier { get; set; } = Guid.NewGuid().ToString(); } 9
  28. Logs Correlation – Across Process Boundaries • Serilog log enricher

    for more advanced use cases • Add a middleware before MVC • Push an existing or new id to LogContext • Send the id via HTTP headers for correlation across process boundaries ASP.NET Core var serilogLoggerConfig = new LoggerConfiguration() .Enrich.FromLogContext() .WriteTo.LiterateConsole("... {CorrelationId} ...") ...; public void Configure(IApplicationBuilder app) { app.UseMiddleware<LogEnricherMiddleware>(); app.UseMvc(); } public class LogEnricherMiddleware { ... public async Task Invoke(HttpContext ctx) { string id; if ((id = ctx.Request.Headers["CorrelationId"].FirstOrDefault()) == null) id = Guid.NewGuid().ToString(); ctx.Response.Headers["CorrelationId"] = id; using (LogContext.PushProperty("CorrelationId", id)) { await _next(ctx); } } }
  29. Graceful Shutdown • Controller actions are not cancelled on server

    shutdown • Use IApplicationLifetime to get a CancellationToken • Dosn‘t fire when client cancels the request • ASP.NET Core v2.0: use HttpContext.RequestAborted to get notified when client is disconnected ASP.NET Core [Route("[controller]")] public class DemoController : Controller { public DemoController(IApplicationLifetime appLifetime) { _appLifetime = appLifetime; } [HttpGet("Do")] public async Task<IActionResult> Do() { CancellationToken cancellationToken = _appLifetime.ApplicationStopping; await Task.Delay(5000, cancellationToken); return Json(DateTime.Now); } }
  30. aka Entity Framework 7 • Cross-platform O/R-Mapper made by Microsoft

    • Supports code-first and database-first • Entity Framework Migrations for updating database schema • Supported database (list is not complete) • MS SQL Server • Postgres • Sqlite • In-Memory (for testing purposes) Entity Framework Core
  31. Code-First • The database context and entity classes are created

    manually Entity Framework Core public class DemoDbContext : DbContext { public DbSet<DemoRecord> Records { get; set; } public DemoDatabaseContext(DbContextOptions<DemoDbContext> options) :base(options) { } } public class DemoRecord { [Key] public Guid Id { get; set; } [Required, MaxLength(100)] public string Name { get; set; } }
  32. Database-First • Add following to the *.csproj • Scaffolding via

    CLI Entity Framework Core <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.1.1"> <PrivateAssets>All</PrivateAssets> </PackageReference> <!--We are using MS SQL Server--> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.Design" Version="1.1.2"> <PrivateAssets>All</PrivateAssets> </PackageReference> <!--If you prefer CLI instead of Package Manager Console--> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" /> </ItemGroup> > dotnet ef dbcontext scaffold "Server=(local);Database=Demo;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer
  33. Migrations • Add to *.csproj with the database context •

    EF Migration requires a parameterless constructor or an implementation of IDbContextFactory • Creates new migration • Update database via CLI, Package Manager Console or via code Entity Framework Core <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.1.1"> <PrivateAssets>All</PrivateAssets> </PackageReference> <!--If you prefer CLI instead of Package Manager Console--> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" /> </ItemGroup> > dotnet ef migrations add Initial_Migration > dotnet ef database update
  34. Migrations - IDbContextFactory • IDbContextFactory has to be in the

    same project as the dabase context • The factory can be internal Assembly Versioning Mismatch: http://weblogs.thinktecture.com/pawel/2017/03/entity-framework-core- migrations-assembly-version-mismatch.html Entity Framework Core internal class EfMigrationDbContextFactory : IDbContextFactory<DemoDbContext> { public DemoDbContext Create(DbContextFactoryOptions options) { var builder = new DbContextOptionsBuilder<DemoDbContext>(); builder.UseSqlServer("server=(local);database=Demo;integrated security=true;"); return new DemoDbContext(builder.Options); } } 10
  35. Dependency Injection • Registers with DI with lifetime „scoped“ •

    Alternative registration with DI Entity Framework Core IServiceCollection services; services.AddDbContext<DemoDbContext>(optionsBuilder => optionsBuilder.UseSqlServer("ConnString")); ContainerBuilder builder; builder.RegisterType<DemoDbContext>().AsSelf().InstancePerLifetimeScope(); builder.Register<DbContextOptions<DemoDbContext>>(context => { return new DbContextOptionsBuilder<DemoDbContext>() .UseSqlServer("ConnString") .Options; }) .AsSelf() .SingleInstance();
  36. Demo • https://github.com/PawelGerr/Presentation-Web- based-Applications-with-.NET-Core Slides • https://speakerdeck.com/pawelgerr Resources .NET Core

    / .NET Standard • https://www.microsoft.com/net/core/platform • https://github.com/dotnet/core • https://docs.microsoft.com/en- us/dotnet/articles/standard/library ASP.NET Core • https://docs.microsoft.com/en-us/aspnet/core/ ASP.NET Core • https://docs.microsoft.com/en-us/ef/core/