Reader small image

You're reading from  Web API Development with ASP.NET Core 8

Product typeBook
Published inApr 2024
PublisherPackt
ISBN-139781804610954
Edition1st Edition
Concepts
Right arrow
Author (1)
Xiaodi Yan
Xiaodi Yan
author image
Xiaodi Yan

Xiaodi Yan is a seasoned software engineer with a proven track record in the IT industry. Since 2015, he has been awarded Microsoft MVP, showcasing his dedication to and expertise in .NET, AI, DevOps, and cloud computing. He is also a Microsoft Certified Trainer (MCT), Azure Solutions Architect Expert, and LinkedIn Learning instructor. Xiaodi often presents at conferences and user groups, leveraging his extensive experience to engage and inspire audiences. Based in Wellington, New Zealand, he spearheads the Wellington .NET User Group, fostering a vibrant community of like-minded professionals. Connect with Xiaodi on LinkedIn to stay updated on his latest insights.
Read more about Xiaodi Yan

Right arrow

Dependency injection

In the preceding example of the controller, there is a _postsService field that is initialized in the constructor method of the controller by using the new() constructor:

private readonly PostsService _postsService;public PostsController()
{
    _postsService = new PostsService();
}

That says the PostsController class depends on the PostsService class, and the PostsService class is a dependency of the PostsController class. If we want to replace PostsService with a different implementation to save the data, we have to update the code of PostsController. If the PostsService class has its own dependencies, they must also be initialized by the PostsController class. When the project grows larger, the dependencies will become more complex. Also, this kind of implementation is not easy to test and maintain.

Dependency injection (DI) is one of the most well-known design patterns in the software development world. It helps decouple classes that depend on each other. You may find the following terms being used interchangeably: Dependency Inversion Principle (DIP), Inversion of Control (IoC), and DI. These terms are commonly confused even though they are related. You can find multiple articles and blog posts that explain them. Some say they are the same thing, but some say not. What are they?

Understanding DI

The Dependency Inversion Principle is one of the SOLID principles in object-oriented (OO) design. It was defined by Robert C. Martin in his book Agile Software Development: Principles, Patterns, and Practices, Pearson, in 2002. The principle states, “high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.”

In the preceding controller, we said PostsController depends on PostsService. The controller is the high-level module, and the service is the low-level module. When the service is changed, the controller must be changed as well. Keep in mind that the term inversion does not mean that the low-level module will depend on the high level. Instead, both of them should depend on abstractions that expose the behavior needed by high-level modules. If we invert this dependency relationship by creating an interface for the service, both the controller and the service will depend on the interface. The implementation of the service can change as long as it respects the interface.

IoC is a programming principle that inverts the flow of control in an application. In traditional programming, custom code is responsible for instantiating objects and controlling the execution of the main function. IoC inverts the flow of control as compared to traditional control flow. With IoC, the framework does the instantiation, calling custom or task-specific code.

It can be used to differentiate a framework from a class library. Normally, the framework calls the application code, and the application code calls the library. This kind of IoC is sometimes referred to as the Hollywood principle: “Don’t call us, we’ll call you.”

IoC is related to DIP, but it is not the same. DIP concerns decoupling dependencies between high-level modules and low-level modules through shared abstractions (interfaces). IoC is used to increase the modularity of the program and make it extensible. There are several technologies to implement IoC, such as Service Locator, DI, the template method design pattern, the strategy design pattern, and so on.

DI is a form of IoC. This term was coined by Martin Fowler in 2004. It separates the concerns of constructing objects and using them. When an object or a function (the client) needs a dependency, it does not know how to construct it. Instead, the client only needs to declare the interfaces of the dependency, and the dependency is injected into the client by external code (an injector). It makes it easier to change the implementation of the dependency. It is often similar to the strategy design pattern. The difference is that the strategy pattern can use different strategies to construct the dependency, while DI typically only uses a single instance of the dependency.

There are three main types of DI:

  • Constructor injection: The dependencies are provided as parameters of the client’s constructor
  • Setter injection: The client exposes a setter method to accept the dependency
  • Interface injection: The dependency’s interface provides an injector method that will inject the dependency into any client passed to it

As you can see, these three terms are related, but there are some differences. Simply put, DI is a technique for achieving IoC between classes and their dependencies. ASP.NET Core supports DI as a first-class citizen.

DI in ASP.NET Core

ASP.NET Core uses constructor injection to request dependencies. To use it, we need to do the following:

  1. Define interfaces and their implementations.
  2. Register the interfaces and the implementations to the service container.
  3. Add services as the constructor parameters to inject the dependencies.

You can download the example project named DependencyInjectionDemo from the folder samples/chapter2/ DependencyInjectionDemo/DependencyInjectionDemo in the chapter's GitHub repository.

Follow the steps below to use DI in ASP.NET Core:

  1. First, we will create an interface and its implementation. Copy the Post.cs file and the PostService.cs file from the previous MyFirstApi project to the DependencyInjectionDemo project. Create a new interface named IPostService in the Service folder, as shown next:
    public interface IPostService{    Task CreatePost(Post item);    Task<Post?> UpdatePost(int id, Post item);    Task<Post?> GetPost(int id);    Task<List<Post>> GetAllPosts();    Task DeletePost(int id);}

    Then, update the PostService class to implement the IPostService interface:

    public class PostsService : IPostService

    You may also need to update the namespace of the Post class and the PostService class.

  2. Next, we can register the IPostService interface and the PostService implementation to the service container. Open the Program.cs file, and you will find that an instance of WebApplicationBuilder named builder is created by calling the WebApplication.CreateBuilder() method. The CreateBuilder() method is the entry point of the application. We can configure the application by using the builder instance, and then call the builder.Build() method to build the WebApplication. Add the following code:
    builder.Services.AddScoped<IPostService, PostsService>();

    The preceding code utilizes the AddScoped() method, which indicates that the service is created once per client request and disposed of upon completion of the request.

  3. Copy the PostsController.cs file from the previous MyFirstApi project to the DependencyInjectionDemo project. Update the namespace and the using statements. Then, update the constructor method of the controller as follows:
    private readonly IPostService _postsService;public PostsController(IPostService postService){    _postsService = postService;}

    The preceding code uses the IPostService interface as the constructor parameter. The service container will inject the correct implementation into the controller.

DI has four roles: services, clients, interfaces, and injectors. In this example, IPostService is the interface, PostService is the service, PostsController is the client, and builder.Services is the injector, which is a collection of services for the application to compose. It is sometimes referred to as a DI container.

The PostsController class requests the instance of IPostService from its constructor. The controller, which is the client, does not know where the service is, nor how it is constructed. The controller only knows the interface. The service has been registered in the service container, which can inject the correct implementation into the controller. We do not need to use the new keyword to create an instance of the service. That says the client and the service are decoupled.

This DI feature is provided in a NuGet package called Microsoft.Extensions.DependencyInjection. When an ASP.NET Core project is created, this package is added automatically. If you create a console project, you may need to install it manually by using the following command:

dotnet add package Microsoft.Extensions.DependencyInjection

If we want to replace the IPostService with another implementation, we can do so by registering the new implementation to the service container. The code of the controller does not need to be changed. That is one of the benefits of DI.

Next, let us discuss the lifetime of services.

DI lifetimes

In the previous example, the service is registered using the AddScoped() method. In ASP.NET Core, there are three lifetimes when the service is registered:

  • Transient: A transient service is created each time it is requested and disposed of at the end of the request.
  • Scoped: In web applications, a scope means a request (connection). A scoped service is created once per client request and disposed of at the end of the request.
  • Singleton: A singleton service is created the first time it is requested or when providing the implementation instance to the service container. All subsequent requests will use the same instance.

To demonstrate the difference between these lifetimes, we will use a simple demo service:

Create a new interface named IDemoService and its implementation named DemoService in the Services folder, as shown next:

IDemoService.cs:

namespace DependencyInjectionDemo.Services;public interface IDemoService
{
    SayHello();
}

DemoService.cs:

namespace DependencyInjectionDemo.Services;public class DemoService : IDemoService
{
    private readonly Guid _serviceId;
    private readonly DateTime _createdAt;
    public DemoService()
    {
        _serviceId = Guid.NewGuid();
        _createdAt = DateTime.Now;
    }
    public string SayHello()
    {
        return $"Hello! My Id is {_serviceId}. I was created at {_createdAt:yyyy-MM-dd HH:mm:ss}.
";
    }
}

The implementation will generate an ID and a time when it was created, and output it when the SayHello() method is called.

  1. Then, we can register the interface and the implementation to the service container. Open the Program.cs file and add the code as follows:
    builder.Services.AddScoped<IDemoService, DemoService>();
  2. Create a controller named DemoController.cs. Now, we can add the service as constructor parameters to inject the dependency:
    [ApiController][Route("[controller]")]public class DemoController : ControllerBase{    private readonly IDemoService _demoService;    public DemoController(IDemoService demoService)    {        _demoService = demoService;    }    [HttpGet]    public ActionResult Get()    {        return Content(_demoService.SayHello());    }}

For this example, if you test the /demo endpoint, you will see the GUID value and the creation time in the output change every time:

http://localhost:5147/> get demoHTTP/1.1 200 OK
Content-Length: 91
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:06:46 GMT
Server: Kestrel
Hello! My Id is 6ca84d82-90cb-4dd6-9a34-5ea7573508ac. I was created at 2023-10-21 11:06:46.
http://localhost:5147/> get demo
HTTP/1.1 200 OK
Content-Length: 91
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:07:02 GMT
Server: Kestrel
Hello! My Id is 9bc5cf49-661d-45bb-b9ed-e0b3fe937827. I was created at 2023-10-21 11:07:02.

We can change the lifetime to AddSingleton(), as follows:

builder.Services.AddSingleton<IDemoService, DemoService>();

The GUID values and the creation time values will be the same for all requests:

http://localhost:5147/> get demoHTTP/1.1 200 OK
Content-Length: 91
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:08:57 GMT
Server: Kestrel
Hello! My Id is a1497ead-bff6-4020-b337-28f1d3af7b05. I was created at 2023-10-21 11:08:02.
http://localhost:5147/> get demo
HTTP/1.1 200 OK
Content-Length: 91
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:09:12 GMT
Server: Kestrel
Hello! My Id is a1497ead-bff6-4020-b337-28f1d3af7b05. I was created at 2023-10-21 11:08:02.

As the DemoController class only requests the IDemoService interface once for each request, we cannot differentiate the behavior between scoped and transient services. Let us look at a more complex example.

  1. You can find the example code in the DependencyInjectionDemo project. There are three interfaces along with their implementations:
    public interface IService{    string Name { get; }    string SayHello();}public interface ITransientService : IService{}public class TransientService : ITransientService{    private readonly Guid _serviceId;    private readonly DateTime _createdAt;    public TransientService()    {        _serviceId = Guid.NewGuid();        _createdAt = DateTime.Now;    }    public string Name => nameof(TransientService);    public string SayHello()    {        return $"Hello! I am {Name}. My Id is {_serviceId}. I was created at {_createdAt:yyyy-MM-dd HH:mm:ss}.";    }}public interface ISingletonService : IService{}public class SingletonService : ISingletonService{    private readonly Guid _serviceId;    private readonly DateTime _createdAt;    public SingletonService()    {        _serviceId = Guid.NewGuid();        _createdAt = DateTime.Now;    }    public string Name => nameof(SingletonService);    public string SayHello()    {        return $"Hello! I am {Name}. My Id is {_serviceId}. I was created at {_createdAt:yyyy-MM-dd HH:mm:ss}.";    }}public interface IScopedService : IService{}public class ScopedService : IScopedService{    private readonly Guid _serviceId;    private readonly DateTime _createdAt;    private readonly ITransientService _transientService;    private readonly ISingletonService _singletonService;    public ScopedService(ITransientService transientService, ISingletonService singletonService)    {        _transientService = transientService;        _singletonService = singletonService;        _serviceId = Guid.NewGuid();        _createdAt = DateTime.Now;    }    public string Name => nameof(ScopedService);    public string SayHello()    {        var scopedServiceMessage = $"Hello! I am {Name}. My Id is {_serviceId}. I was created at {_createdAt:yyyy-MM-dd HH:mm:ss}.";        var transientServiceMessage = $"{_transientService.SayHello()} I am from {Name}.";        var singletonServiceMessage = $"{_singletonService.SayHello()} I am from {Name}.";        return            $"{scopedServiceMessage}{Environment.NewLine}{transientServiceMessage}{Environment.NewLine}{singletonServiceMessage}";    }}
  2. In the Program.cs file, we can register them to the service container as follows:
    builder.Services.AddScoped<IScopedService, ScopedService>();builder.Services.AddTransient<ITransientService, TransientService>();builder.Services.AddSingleton<ISingletonService, SingletonService>();
  3. Then, create a controller named LifetimeController.cs. The code is shown next:
    [ApiController][Route("[controller]")]public class LifetimeController : ControllerBase{    private readonly IScopedService _scopedService;    private readonly ITransientService _transientService;    private readonly ISingletonService _singletonService;    public LifetimeController(IScopedService scopedService, ITransientService transientService,        ISingletonService singletonService)    {        _scopedService = scopedService;        _transientService = transientService;        _singletonService = singletonService;    }    [HttpGet]    public ActionResult Get()    {        var scopedServiceMessage = _scopedService.SayHello();        var transientServiceMessage = _transientService.SayHello();        var singletonServiceMessage = _singletonService.SayHello();        return Content(            $"{scopedServiceMessage}{Environment.NewLine}{transientServiceMessage}{Environment.NewLine}{singletonServiceMessage}");    }}

In this example, ScopedService has two dependencies: ITransientService and ISingletonService. So, when ScopedService is created, it will ask for the instances of these dependencies from the service container. On the other hand, the controller also has dependencies: IScopedService, ITransientService, and ISingletonService. When the controller is created, it will ask for these three dependencies. That means ITransientService and ISingletonService will be needed twice for each request. But let us check the output of the following requests:

http://localhost:5147/> get lifetimeHTTP/1.1 200 OK
Content-Length: 625
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:20:44 GMT
Server: Kestrel
Hello! I am ScopedService. My Id is df87d966-0e86-4f08-874f-ba6ce71de560. I was created at 2023-10-21 11:20:44.
Hello! I am TransientService. My Id is 77e29268-ad48-423c-94e5-de1d09bd3ba5. I was created at 2023-10-21 11:20:44. I am from ScopedService.
Hello! I am SingletonService. My Id is 95a44c5b-8678-48c6-a2f0-cc6b90423773. I was created at 2023-10-21 11:20:44. I am from ScopedService.
Hello! I am TransientService. My Id is e77564d1-e146-4d29-b74b-a07f8f6640c1. I was created at 2023-10-21 11:20:44.
Hello! I am SingletonService. My Id is 95a44c5b-8678-48c6-a2f0-cc6b90423773. I was created at 2023-10-21 11:20:44.
http://localhost:5147/> get lifetime
HTTP/1.1 200 OK
Content-Length: 625
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:20:57 GMT
Server: Kestrel
Hello! I am ScopedService. My Id is e5f802ed-5e4c-4abd-9213-8f13f97c1008. I was created at 2023-10-21 11:20:57.
Hello! I am TransientService. My Id is daccb91b-438f-4561-9c86-13b02ad8e358. I was created at 2023-10-21 11:20:57. I am from ScopedService.
Hello! I am SingletonService. My Id is 95a44c5b-8678-48c6-a2f0-cc6b90423773. I was created at 2023-10-21 11:20:44. I am from ScopedService.
Hello! I am TransientService. My Id is 94e9e6c1-729a-4033-8a27-550ea10ba5d0. I was created at 2023-10-21 11:20:57.
Hello! I am SingletonService. My Id is 95a44c5b-8678-48c6-a2f0-cc6b90423773. I was created at 2023-10-21 11:20:44.

We can see that in each request, ScopedService was created once, while ITransientService was created twice. In both requests, SingletonService was created only once.

Group registration

As the project grows, we may have more and more services. If we register all services in Program.cs, this file will be very large. For this case, we can use group registration to register multiple services at once. For example, we can create a service group named LifetimeServicesCollectionExtensions.cs:

public static class LifetimeServicesCollectionExtensions{
    public static IServiceCollection AddLifetimeServices(this IServiceCollection services)
    {
        services.AddScoped<IScopedService, ScopedService>();
        services.AddTransient<ITransientService, TransientService>();
        services.AddSingleton<ISingletonService, SingletonService>();
        return services;
    }
}

This is an extension method for the IServiceCollection interface. It is used to register all services at once in the Program.cs file:

// Group registrationbuilder.Services.AddLifetimeServices();

In this way, the Program.cs file will be smaller and easier to read.

Action injection

Sometimes, one controller may need many services but may not need all of them for all actions. If we inject all the dependencies from the constructor, the constructor method will be large. For this case, we can use action injection to inject dependencies only when needed. See the following example:

[HttpGet]public ActionResult Get([FromServices] ITransientService transientService)
{
  ...
}

The [FromServices] attribute enables the service container to inject dependencies when needed without using constructor injection. However, if you find that a service needs a lot of dependencies, it may indicate that the class has too many responsibilities. Based on the Single Responsibility Principle (SRP), consider refactoring the class to split the responsibilities into smaller classes.

Keep in mind that this kind of action injection only works for actions in the controller. It does not support normal classes. Additionally, since ASP.NET Core 7.0, the [FromServices] attribute can be omitted as the framework will automatically attempt to resolve any complex type parameters registered in the DI container.

Keyed services

ASP.NET Core 8.0 introduces a new feature known as keyed services, or named services. This feature allows developers to register services with a key, allowing them to access the service with that key. This makes it easier to manage multiple services that implement the same interface within an application, as the key can be used to identify and access the service.

For example, we have a service interface named IDataService:

public interface IDataService{
    string GetData();
}

This IDataService interface has two implementations: SqlDatabaseService and CosmosDatabaseService:

public class SqlDatabaseService : IDataService{
    public string GetData()
    {
        return "Data from SQL Database";
    }
}
public class CosmosDatabaseService : IDataService
{
    public string GetData()
    {
        return "Data from Cosmos Database";
    }
}

We can register them to the service container using different keys:

builder.Services.AddKeyedScoped<IDataService, SqlDatabaseService>("sqlDatabaseService");builder.Services.AddKeyedScoped<IDataService, CosmosDatabaseService>("cosmosDatabaseService");

Then, we can inject the service by using the FromKeyedServices attribute:

[ApiController][Route("[controller]")]
public class KeyedServicesController : ControllerBase
{
    [HttpGet("sql")]
    public ActionResult GetSqlData([FromKeyedServices("sqlDatabaseService")] IDataService dataService) =>
        Content(dataService.GetData());
    [HttpGet("cosmos")]
    public ActionResult GetCosmosData([FromKeyedServices("cosmosDatabaseService")] IDataService dataService) =>
        Content(dataService.GetData());
}

The FromKeyedServices attribute is used to inject the service by using the specified key. Test the API with HttpRepl, and you will see the output as follows:

http://localhost:5147/> get keyedServices/sqlHTTP/1.1 200 OK
Content-Length: 22
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:48:49 GMT
Server: Kestrel
Data from SQL Database
http://localhost:5147/> get keyedServices/cosmos
HTTP/1.1 200 OK
Content-Length: 25
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:48:54 GMT
Server: Kestrel
Data from Cosmos Database

The keyed services can be used to register singleton or transient services as well. Just use the AddKeyedSingleton() or AddKeyedTransient() method respectively; for example:

builder.Services.AddKeyedSingleton<IDataService, SqlDatabaseService>("sqlDatabaseService");builder.Services.AddKeyedTransient<IDataService, CosmosDatabaseService>("cosmosDatabaseService");

It is important to note that if an empty string is passed as the key, a default implementation for the service must be registered with a key of an empty string, otherwise the service container will throw an exception.

Microsoft releases new versions of .NET SDKs frequently. If you encounter a different version number, that is acceptable.

The preceding command will list all the available SDKs on your machine. For example, it may show the following output if have multiple .NET SDKs installed.

Important note

Every Microsoft product has a lifecycle. .NET and .NET Core provides Long-term support (LTS) releases that get 3 years of patches and free support. When this book was written, .NET 7 is still supported, until May 2024. Based on Microsoft’s policy, even numbered releases are LTS releases. So .NET 8 is the latest LTS release. The code samples in this book are written with .NET 8.0.

When you use VS Code to open the project, the C# Dev Kit extension can create a solution file for you. This feature makes VS Code more friendly to C# developers. You can see the following structure in the Explorer view:

It uses the Swashbuckle.AspNetCore NuGet package, which provides the Swagger UI to document and test the APIs.

Follow the steps below to use DI in ASP.NET Core:

We can see that in each request, ScopedService was created once, while ITransientService was created twice. In both requests, SingletonService was created only once.

Using primary constructors to inject dependencies

Beginning with .NET 8 and C# 12, we can use the primary constructor to inject dependencies. A primary constructor allows us to declare the constructor parameters directly in the class declaration, instead of using a separate constructor method. For example, we can update the PostsController class as follows:

```csharppublic class PostsController(IPostService postService) : ControllerBase
{
    // No need to define a private field to store the service 
    // No need to define a constructor method
}
```

You can find a sample named PrimaryConstructorController.cs in the Controller folder of the DependencyInjectionDemo project.

When using the primary constructor in a class, note that the parameters passed to the class declaration cannot be used as properties or members. For example, if a class declares a parameter named postService in the class declaration, it cannot be accessed as a class member using this.postService or from external code. To learn more about the primary constructor, please refer to the documentation at https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors.

Primary constructors can save us from writing fields and constructor methods. So, we’ll use them in the following examples.

Do not use new to create service B, otherwise, service A will be tightly coupled with service B.

Resolving a service when the app starts

If we need a service in the Program.cs file, we cannot use constructor injection. For this situation, we can resolve a scoped service for a limited duration at app startup, as follows:

var app = builder.Build();using (var serviceScope = app.Services.CreateScope())
{
    var services = serviceScope.ServiceProvider;
    var demoService = services.GetRequiredService<IDemoService>();
    var message = demoService.SayHello();
    Console.WriteLine(message);
}

The preceding code creates a scope and resolves the IDemoService service from the service container. Then, it can use the service to do something. After the scope is disposed of, the service will be disposed of as well.

DI tips

ASP.NET Core uses DI heavily. The following are some tips to help you use DI:

  • When designing your services, make the services as stateless as possible. Do not use static classes and members unless you have to do so. If you need to use a global state, consider using a singleton service instead.
  • Carefully design dependency relationships between services. Do not create a cyclic dependency.
  • Do not use new to create a service instance in another service. For example, if service A depends on service B, the instance of service B should be injected into service A with DI. Do not use new to create service B, otherwise, service A will be tightly coupled with service B.
  • Use a DI container to manage the lifetime of services. If a service implements the IDisposable interface, the DI container will dispose of the service when the scope is disposed of. Do not manually dispose of it.
  • When registering a service, do not use new to create an instance of the service. For example, services.AddSingleton(new ExampleService()); registers a service instance that is not managed by the service container. So, the DI framework will not be able to dispose of the service automatically.
  • Avoid using the service locator pattern. If DI can be used, do not use the GetService() method to obtain a service instance.

You can learn more about the DI guidelines at https://docs.microsoft.com/zh-cn/dotnet/core/extensions/dependency-injection-guidelines.

Why there is no configuration method for the logger in the template project?

ASP.NET Core provides a built-in DI implementation for the logger. When the project was created, logging was registered by the ASP.NET Core framework. Therefore, there is no configuration method for the logger in the template project. Actually, there are more than 250 services that are automatically registered by the ASP.NET Core framework.

Can I use third-party DI containers?

It is highly recommended that you use the built-in DI implementation in ASP.NET Core. But if you need any specific features that it does not support, such as property injection, Func<T> support for lazy initialization, and so on, you can use third-party DI containers, such as Autofac (https://autofac.org/).

Previous PageNext Page
You have been reading a chapter from
Web API Development with ASP.NET Core 8
Published in: Apr 2024Publisher: PacktISBN-13: 9781804610954
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Xiaodi Yan

Xiaodi Yan is a seasoned software engineer with a proven track record in the IT industry. Since 2015, he has been awarded Microsoft MVP, showcasing his dedication to and expertise in .NET, AI, DevOps, and cloud computing. He is also a Microsoft Certified Trainer (MCT), Azure Solutions Architect Expert, and LinkedIn Learning instructor. Xiaodi often presents at conferences and user groups, leveraging his extensive experience to engage and inspire audiences. Based in Wellington, New Zealand, he spearheads the Wellington .NET User Group, fostering a vibrant community of like-minded professionals. Connect with Xiaodi on LinkedIn to stay updated on his latest insights.
Read more about Xiaodi Yan