Getting started with ASP.NET Core

Translated into:UA
EN

Serhii Yanchuk

Serhii works at Binary Studio as backend developer. He enjoys coding and making three-point shots in basketball. Therefore, if you are from Kyiv and want to combine a conversation about code with a game in basketball, contact him :D

Hello! 👋
I'll give you tips and tricks to better understand how our tech stack works. You'll have an ASP.NET Core web application by the end of this lesson. This is an introductory lecture, so:
  1. some things may have to be googled yourself;
  2. maybe you already know all this (repetition won't hurt anyway).
Let's go!
Level 1 back to top

Intro to .NET

Difficulty: No worries. Objectives: Familiarize yourself with .NET fundamentals because you are going to be buddies.
How Microsoft came up with what we now call .NET

There were times when the term ".NET" meant Windows platform only. This platforms is called .NET Framework. It imposed some restrictions for deploying because most machines have Linux as an operating system. So the guys in Microsoft gathered for a meeting and came up with .NET Core. The main idea of the framework is cross-platform apps, which means you can host your app on a variety of OS. Starting with .NET 5, the platform version became known as .NET (without the use of "Core" in the name). Furthermore, it’s open-source, hence it has great community support.

.NET advantages
  1. Cross-platform
    Runs on Windows, Linux, macOS
  2. Flexible deployment
    The framework can be included in your app or installed side-by-side user-or machine-wide
  3. Command-line tools
    .NET has great CLI, therefore all product scenarios can be executed using command-line
  4. Compatibility
    .NET is compatible with .NET Framework, Xamarin, and Mono, via the .NET Standard Library
  5. Open-source
    The .NET platform is open source, using MIT and Apache 2 licenses. You are welcome to contribute
  6. Supported by Microsoft
    Huge corporation forces .NET to develop and gain new features
What you can build with .NET
Web apps which implement Model-View-Controller pattern and use Razor for HTML markdown with C# snippets
Backend app which implements business logic and can be used by any client (whether it’s Angular-, React-, or WhateverFramework-based frontend, mobile apps, etc.)
What you need to start
  • .NET SDK includes everything you need to build and run .NET applications. Since you are not limited to Windows only, you can choose whatever IDE or text editor you want (Visual Studio, Visual Studio Code, JetBrains Rider, Sublime, Atom, and so on). You are able to write code using your favorite tool and execute needed actions using the CLI. Some useful CLI commands are:

    • dotnet new — initializes a sample console C# project
    • dotnet restore — restores the dependencies for a given application
    • dotnet build — builds a .NET application
    • dotnet publish — publishes a .NET portable or self-contained application
    • dotnet run — runs the application from source
    • dotnet test — runs tests using a test runner specified in the project.json
    • dotnet pack — creates a NuGet package of your code
  • .NET Runtime includes just the resources required to run existing .NET applications (this runtime is included in the SDK)

Level 2 back to top

Creating an app

Difficulty: Gets warmer, typing is required. Objectives: Learn dotnet new, understand the anatomy of ASP.NET Core app.
What is ASP.NET Core [1] [2]

ASP.NET is a popular web-development framework for building web apps on the .NET platform. ASP.NET Core is the open-source version of ASP.NET, that runs on macOS, Linux, and Windows. It was first released in 2016 and is a re-design of earlier Windows-only versions of ASP.NET. In comparison to ASP.NET, Core version provides:

  • Cleaner and modular architecture
  • Tighter security
  • Reduced servicing
  • Improved performance
Why use it
  • Integration of modern client-side frameworks and development workflows
  • A cloud-ready environment-based configuration system
  • Built-in dependency injection
  • New light-weight and modular HTTP request pipeline
  • Ability to host on IIS or self-host in your own process
  • Built on .NET, which supports true side-by-side app versioning
  • Ships entirely as NuGet packages
  • New tooling that simplifies modern web development
  • Build and run cross-platform ASP.NET apps on Windows, Linux, and macOS
  • Open-source and community focused
Create a new app by running dotnet new webapi or using features of your IDE [1]
  • This is how the entry point looks like:
    public static void Main(string[] args)
    {
      var builder = WebApplication.CreateBuilder(args);
    
      // Add services to the container.
    
      builder.Services.AddControllers();
    
      var app = builder.Build();
    
      // Configure the HTTP request pipeline.
    
      app.UseHttpsRedirection();
    
      app.UseAuthentication();
      app.UseAuthorization();
    
      app.MapControllers();
    
      app.Run();
    }
    Here we can configure services that are used by your application and define how the application will respond to individual HTTP requests (you can setup pipelines which will process requests).
Level 3 back to top

Controllers and services

Difficulty: No kidding. Objectives: Learn that services are no joke and business logic shouldn't live in controllers.

I’m going to show you how we can write API using ASP.NET Core Web API.

  1. First, let’s create the simple application that retrives all binary students. Create new files called BinaryStudentsController.cs in the Controllers folder and BinaryStudent.cs where you want

    • using Microsoft.AspNetCore.Mvc;
      using BSA_ASP.NET.Business.Models;
      
      namespace BSA_ASP.NET.Controllers;
      
      [ApiController]
      [Route("[controller]")]
      public class BinaryStudentsController : ControllerBase
      {
        public BinaryStudentsController() { }
      
        [HttpGet]
        public IEnumerable<BinaryStudent> GetStudents() => new BinaryStudent[]
          {
            new BinaryStudent { Id = 1, FirstName = "Serhii", LastName = "Yanchuk", Age = 21 },
            new BinaryStudent { Id = 2, FirstName = "Vadym", LastName = "Kolesnyk", Age = 21 }
          };
      }
    • namespace BSA_ASP.NET.Business.Models;
      
      public class BinaryStudent
      {
        public int Id { get; set; }
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
        public int Age { get; set; }
      }
  2. You can use dotnet run command to start the server. To make sure that our API is working, we should send a request to the BinaryStudentsController. Open https://localhost:7088/binarystudents — you should receive students in JSON format as a response (you can find your port number in launchSettings.json file).

    You can use Postman to test your API or, for example, use an extension for VSCode ― REST Client
  3. Let’s practice a bit more and complicate our application, add more endpoints and dependencies. The modified controller will be a complete CRUD (C - create, R - read, U - update, D - delete) controller. Writing business logic in controllers isn’t the best idea, so it would be better if we created a separate service for this purpose.

    • using BSA_ASP.NET.Business.Models;
      
      namespace BSA_ASP.NET.Business.Interfaces;
      
      public interface IBinaryStudentService
      {
        public List<BinaryStudent> Get(string? filter);
        public BinaryStudent GetById(int id);
        public BinaryStudent Add(BinaryStudent student);
        public BinaryStudent Update(BinaryStudent student);
        public void Delete(int id);
      }
    • using BSA_ASP.NET.Business.Interfaces;
      using BSA_ASP.NET.Business.Models;
      
      namespace BSA_ASP.NET.Business.Services;
      
      public class BinaryStudentService: IBinaryStudentService
      {
        public BinaryStudentService()
        {
          _students = new List<BinaryStudent>
          {
            new BinaryStudent { Id = 1, FirstName = "Serhii", LastName = "Yanchuk", Age = 21 },
            new BinaryStudent { Id = 2, FirstName = "Vadym", LastName = "Kolesnyk", Age = 21 }
          };
        }
      
        public List<BinaryStudent> Get(string? filter) => 
          string.IsNullOrEmpty(filter) ? 
            _students : 
            _students.Where(s => s.LastName.Contains(filter, StringComparison.OrdinalIgnoreCase)).ToList();
      
        public BinaryStudent GetById(int id) => _students.SingleOrDefault(s => s.Id == id);
      
        public BinaryStudent Add(BinaryStudent student)
        {
          student.Id = _students.Max(s => s.Id) + 1;
          _students.Add(student);
          return student;
        }
      
        public BinaryStudent Update(BinaryStudent student)
        {
          var studentToUpdate = GetById(student.Id);
      
          if (studentToUpdate is not null)
          {
            studentToUpdate.FirstName = student.FirstName;
            studentToUpdate.LastName = student.LastName;
            studentToUpdate.Age = student.Age;
          }
      
          return studentToUpdate;
        }
      
        public void Delete(int id)
        {
          _students = _students.Where(s => s.Id != id).ToList();
        }
      
        private List<BinaryStudent> _students;
      }
    • using BSA_ASP.NET.Business.Interfaces;
      using BSA_ASP.NET.Business.Models;
      using Microsoft.AspNetCore.Mvc;
      
      namespace BSA_ASP.NET.Controllers;
      
      [ApiController]
      [Route("[controller]")]
      public class BinaryStudentsController : ControllerBase
      {
        public BinaryStudentsController(IBinaryStudentService studentService) 
        {
          _studentService = studentService;
        }
      
        [HttpGet] // https://localhost:7088/binarystudents?filter=yan
        public IEnumerable<BinaryStudent> GetStudents([FromQuery] string? filter = default) => _studentService.Get(filter);
      
        [HttpGet("{id}")] // https://localhost:7088/binarystudents/1
        public BinaryStudent GetStudent(int id) => _studentService.GetById(id);
      
        [HttpPost] // https://localhost:7088/binarystudents
        public BinaryStudent AddStudent([FromBody] BinaryStudent student) => _studentService.Add(student);
      
        [HttpPut] // https://localhost:7088/binarystudents
        public BinaryStudent UpdateStudent([FromBody] BinaryStudent student) => _studentService.Update(student);
      
        [HttpDelete("{id}")] // https://localhost:7088/binarystudents/3
        public void DeleteStudent(int id) => _studentService.Delete(id);
      
        private IBinaryStudentService _studentService;
      }
    • We need to register our service. Read more about this in Dependency Injection paragraph
      using BSA_ASP.NET.Business.Interfaces;
      using BSA_ASP.NET.Business.Services;
      
      namespace BSA_ASP.NET;
      
      public class Program
      {
        public static void Main(string[] args)
        {
          var builder = WebApplication.CreateBuilder(args);
      
          // Add services to the container.
          ConfigureServices(builder.Services);
      
          var app = builder.Build();
      
          // Configure the HTTP request pipeline.
          ConfigurePipeline(app);
      
          app.Run();
        }
      
        private static void ConfigureServices(IServiceCollection services)
        {
          services.AddControllers();
          services.AddSingleton<IBinaryStudentService, BinaryStudentService>();
        }
      
        private static void ConfigurePipeline(WebApplication app)
        {
          app.UseHttpsRedirection();
      
          app.UseAuthentication();
          app.UseAuthorization();
      
          app.MapControllers();
        }
      }
  4. Run the application and test three endpoints to make sure everything works.

Level 4 back to top

Additional features

Difficulty: Like the first one times five? Objectives: Troubleshoot, debug, extend, and wire up your app better.
A short review of Dependency Injection (Singleton, Scoped, Transient)

How can we pass dependencies? The answer is DI or Dependency injection. Dependency injection (DI) — software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies. In our case BinaryStudentsController depends on BinaryStudentService. There might be a case when we want to use another implementation. In this case, we would need to go through our app and change class name manually in all places where it’s used. With DI we can do it easily in one place (in Program.cs). Moreover, in .NET DI container can create instances of dependencies. Otherwise, we would need to do it manually in every service. In addition to that, .NET DI container takes on the responsibility of creating an instance of the dependency and disposing of it when it's no longer needed. There are a few service lifetime options:

  1. Singleton — services are created the first time they're requested (or when an instance is specified with the service registration during web app configuration). Every subsequent request uses the same instance.
  2. Scoped — services are created once per client request (connection).
  3. Transient — services are created each time they're requested from the service container. This lifetime works best for lightweight, stateless services.

Let’s apply Singleton to our BinaryStudentService so that we use the same collection of students:

  1. It can be done using IServiceCollection in Program.cs:
    builder.Services.AddSingleton<BinaryStudentService>();
  2. Going further, we can create an interface for BinaryStudentService and bind it to implementation. It will let us switch services in one place. Change the previous version of service registration to the next one:
    builder.Services.AddSingleton<IBinaryStudentService, BinaryStudentService>();
Middleware

Middleware is software that's assembled into an app pipeline to handle requests and responses. Long story short: the app applies all middlewares one by one for each request.

php-fpm_nginx

For example, if you want to log all the requests, you can add a pipeline (in the example below Console.WriteLine is used for simplicity, you can use any logging framework there). So, add the next code into the configuration of pipeline in Program.cs:

app.Use(async(context, next) => 
{
  Console.WriteLine("Started handling request");
  await next.Invoke();
  Console.WriteLine("Finished handling request");
});
Now start the app, send a request and see the console output.
Routing

Routing is responsible for mapping request URIs to endpoint selectors and dispatching incoming requests to endpoints. Routes are defined in the app and configured when the app starts. A route can optionally extract values from the URL contained in the request, and these values can then be used for request processing. Basically, routing is a middleware. Using the picture above, imagine how request comes to the server, then it’s being processed by a chain of middlewares and then routing middleware matches URL to controller and method names. In WebAPI we define route using the Route attribute:

[ApiController]
[Route("[controller]")]
public class BinaryStudentsController : ControllerBase
You can set a specific string, or you can use **[controller]** placeholder. And if you follow the naming convention and your class name has the **"Controller"** prefix at the end, then instead of the placeholder the name without the prefix will be substituted There is same attribute for methods in the controller.
Minimal API

The Minimal API is a new way to build APIs without a lot of controller-based API code. You can define endpoints using these extension methods in the configuration of pipeline:

  • MapGet;
  • MapPost;
  • MapPut;
  • MapDelete;
  • MapMethods.
app.MapGet("/binarystudents", (IBinaryStudentService studentService) => studentService.Get(string.Empty));
The first parameter is path which the endpoint handles and the second is handler of delegate type. This is convenient if creating an entire controller is pointless. It's also handy if you need to quickly test an endpoint.
Status codes

Of course, returning only codes 200 or 500 is cool. But there are a lot of informative codes, so why not use them? One of the ways to specify status codes for the responses to the client is using the methods of the ControllerBase class, from which the API controller inherits. For example, the following methods:

  • Ok - 200;
  • Created - 201;
  • BadRequest - 400;
  • NotFound - 404;
  • Forbid - 403;
  • StatusCode - any code.

In order to use them, we have to wrap the return value type in the ActionResult class:

[HttpGet("{id}")] // https://localhost:7088/binarystudents/1
public ActionResult<BinaryStudent> GetStudent(int id) => Ok(_studentService.GetById(id));

[HttpPost] // https://localhost:7088/binarystudents
public ActionResult<BinaryStudent> AddStudent([FromBody] BinaryStudent student)
{
    var addedStudent = _studentService.Add(student);
    return Created($"~binarystudents/{addedStudent.Id}", addedStudent);
}

― That's all, folks! 🐷

That's it in brief about ASP.NET Core. That's all, thank you very much for your attention🎩! Ask questions and leave feedback on the lecture on the academy's website. Good luck with homework, see you soon, bye!