Getting started with ASP.NET Core
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:
- some things may have to be googled yourself;
- maybe you already know all this (repetition won't hurt anyway).
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
- Cross-platform
Runs on Windows, Linux, macOS - Flexible deployment
The framework can be included in your app or installed side-by-side user-or machine-wide - Command-line tools
.NET has great CLI, therefore all product scenarios can be executed using command-line - Compatibility
.NET is compatible with .NET Framework, Xamarin, and Mono, via the .NET Standard Library - Open-source
The .NET platform is open source, using MIT and Apache 2 licenses. You are welcome to contribute - Supported by Microsoft
Huge corporation forces .NET to develop and gain new features
What you can build with .NET
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# projectdotnet restore
— restores the dependencies for a given applicationdotnet build
— builds a .NET applicationdotnet publish
— publishes a .NET portable or self-contained applicationdotnet run
— runs the application from sourcedotnet test
— runs tests using a test runner specified in the project.jsondotnet 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)
Creating an app
Difficulty: Gets warmer, typing is required. Objectives: Learndotnet 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: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).
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(); }
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.
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; } }
You can use
dotnet run
command to start the server. To make sure that our API is working, we should send a request to theBinaryStudentsController
. 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 ClientLet’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(); } }
Run the application and test three endpoints to make sure everything works.
- https://localhost:7088/binarystudents?filter=yan - GET request, retrieving those students whose last names contain the filter string;
- https://localhost:7088/binarystudents/1 - GET request, retrieving the student by their id;
- https://localhost:7088/binarystudents - POST request, adding the new student that you define in the body of request in JSON format.
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:
- 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.
- Scoped — services are created once per client request (connection).
- 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:
- It can be done using IServiceCollection in Program.cs:
builder.Services.AddSingleton<BinaryStudentService>();
- 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.
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");
});
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
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));
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);
}