Services

Attribution

This tutorial is a derivative of the Angular Tour Of Heroes App and Tutorial under CC BY 4.0..

The Tour of Heroes Heroes component is currently getting and displaying fake data.

After the refactoring in this tutorial, the Heroes component will be lean and focused on supporting the view. It will also be easier to unit-test with a mock service.

Why services?

UI components shouldn't fetch or save data directly and they certainly shouldn't knowingly generate fake data. They should focus on presenting data and delegate data access to a service. Classes that provide services to components are also known as dependencies, in that the component depends on the service for data that the component is designed to display.

In this tutorial, you'll create a HeroService that all application classes can use to get heroes. Instead of creating that service with new, you'll rely on Blazor's dependency injection system to inject it into the Heroes component.

Services are a great way to share information among classes that don't know each other. You'll also create a MessageService and inject it in two places:

  • in HeroService which uses the service to send a message
  • in a Messages component which displays that message

Create the HeroService

  1. Add a new folder to the root of the project and name it Services.

  2. Select the folder and press Ctrl+Shift+A. From the Add New Item dialog, choose Interface and name it IHeroService.cs.

  3. Replace the existing code with the following:

    using System.Collections.Generic;
    using System.Threading.Tasks;
    using TourOfHeroes.Models;
    
    namespace TourOfHeroes.Services
    {
        public interface IHeroService
        {
            Task<List<Hero>> GetHeroes();
        }
    }
    

    An interface is an abstraction. It specifies the signatures of methods without providing an implementation. Any class that implements the interface is responsible for proving an implementation of the specified methods.

    You can program to an interface. What this means in practice is that the consumer of the service (e.g. the Heroes component) will support code that calls methods on an interface without having to know anything about the actual implementation of the interface. This is also known as decoupling the component from the implementation of the dependency.

  4. Add another new file, this time a Class file and name it HeroService.cs.

  5. Replace the existing code with the following:

    using System.Collections.Generic;
    using System.Threading.Tasks;
    using TourOfHeroes.Models;
    
    namespace TourOfHeroes.Services
    {
        public class HeroService : IHeroService
        {
            List<Hero> heroes = new List<Hero>{
                new Hero { Id = 11, Name = "Dr Nice" },
                new Hero { Id = 12, Name = "Narco" },
                new Hero { Id = 13, Name = "Bombasto" },
                new Hero { Id = 14, Name = "Celeritas" },
                new Hero { Id = 15, Name = "Magneta" },
                new Hero { Id = 16, Name = "RubberMan" },
                new Hero { Id = 17, Name = "Dynama" },
                new Hero { Id = 18, Name = "Dr IQ" },
                new Hero { Id = 19, Name = "Magma" },
                new Hero { Id = 20, Name = "Tornado" }
            };
    
            public async Task<List<Hero>> GetHeroes()
            {
                return await Task.Run(() => { return heroes;});
            }
        }
    }
    

    The HeroService implements the interface that you created earlier, and therefore provides a valid implementation of the GetHeroes method, returning a List<Hero>. The service could get hero data from anywhere — a web service, local storage, or a mock data source (as in this example).

    Removing data access from UI components into separate classes means you can change your mind about the implementation anytime, without touching any components. They don't know how the service works. The implementation in this tutorial continues to deliver mock heroes, but later, the data will come from a web API. The GetHeroes method is asynchronous. In this particular case, it doesn't need to be. The asynchronicity is contrived. You will use asynchronous code to call into the web service later. In the meantime, the asynchronous methods are placeholders for that eventuality.

  6. Open Program.cs in the root of the project and add the highlighted lines of code:

    using Microsoft.AspNetCore.Blazor.Hosting;
    using Microsoft.Extensions.DependencyInjection;
    using System.Threading.Tasks;
    using TourOfHeroes.Lib.Services;
    
    namespace TourOfHeroes
    {
        public class Program
        {
            public static async Task Main(string[] args)
            {
                var builder = WebAssemblyHostBuilder.CreateDefault(args);
                builder.RootComponents.Add<App>("app");
    
                builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
                builder.Services.AddSingleton<IHeroService, HeroService>();
                await builder.Build().RunAsync();
            }
        }
    }
    

    The HeroService is registered with the dependency injection system as a singleton. An instance of the HeroService class will be created when first requested and then the same instance will be provided to code that calls methods on IHeroService as needed.

  7. Add the following line to the _Imports.razor file in the project root:

    @using TourOfHeroes.Services
    

    The HeroService is now ready to plug into the Heroes component.

  8. Alter the Heroes component, making the changes highlighted below:

    @page "/heroes"
    @inject IHeroService  heroService
    
    <h2>My Heroes</h2>
    <ul class="heroes">
        @foreach (var hero in heroes)
        {
            <li @onclick="@(e => onSelect(hero))" class="@(hero == selectedHero ? "selected" : "")">
                <span class="badge">@hero.Id</span> @hero.Name
            </li>
        }
    </ul>
    
    <HeroDetail hero="@selectedHero"></HeroDetail>
    
    @code {
        List<Hero> heroes { get; set; } = new List<Hero>();
    
        Hero selectedHero { get; set; }
    
        void onSelect(Hero hero)
        {
            selectedHero = hero;
        }
    
        protected override async Task OnInitializedAsync()
        {
            heroes = await heroService.GetHeroes();
        }
    }
    

    The first change illustrates the use of the @inject directive to inject the HeroService into the component. Note that it references the interface. the DI system will resolve the implementation at runtime, based on the registration you set up earlier in the Main method of the Program class.

    The second change see the data removed from the component and the List<Hero> initialised to an empty list instead.

    The final change sees a method being added to the component - OnInitializedAsync, which is executed when the component has been initialised. This is the place to obtain data from the service.

Now if you run the application and navigate to /heroes, it should continue to work as before.

Show messages

This section guides you through the following:

  • adding a Messages component that displays app messages at the bottom of the screen
  • creating an injectable, app-wide MessageService for sending messages to be displayed
  • injecting MessageService into the HeroService
  • displaying a message when HeroService fetches heroes successfully

Create the MessageService

  1. Add a new interface to the Services folder, and name it IMessageService.cs.

  2. Replace the content with the following:

    using System.Collections.Generic;
    
    namespace TourOfHeroes.Services
    {
        public interface IMessageService
        {
            void Add(string message);
            void Clear();
            List<string> Messages { get; set; }
        }
    }
    
  3. Add a new C# class file to the Services folder and name it MessageService.cs.

  4. Replace the content with the following:

    using System.Collections.Generic;
    
    namespace TourOfHeroes.Services
    {
        public class MessageService : IMessageService
        {
            public List<string> Messages { get; set; } = new List<string>();
            public void Add(string message)
            {
                Messages.Add(message);
            }
    
            public void Clear()
            {
                Messages.Clear();
            }
        }
    }
    

    The service exposes its cache of messages and two methods: one to Add() a message to the cache and another to Clear() the cache.

  5. In HeroService, inject the MessageService and assign it to a private field:

    public class HeroService : IHeroService
    {
        private readonly IMessageService messageService;
        public HeroService(IMessageService messageService) => this.messageService = messageService;
    
        List<Hero> heroes = new List<Hero>{
        ...
    

    This is a typical "service-in-service" scenario: you inject the MessageService into the HeroService which is injected into the Heroes component. The MessageService is injected via the newly added HeroService constructor. It is assigned to the also newly added private field via an expression body definition.

  6. Register the MessageService as a singleton in Program.cs:

    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");
            builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
            builder.Services.AddSingleton<IHeroService, HttpHeroService>();
            builder.Services.AddSingleton<IMessageService, MessageService>();
            await builder.Build().RunAsync();
        }    
    }
    
  7. Modify the GetHeroes method in the HeroService method to send a message when the heroes are fetched:

    public async Task<List<Hero>> GetHeroes()
    {
        messageService.Add("HeroService: fetched heroes");
        return await Task.Run(() => { return heroes; });
    }
    

Display the message from HeroService

The Messages component should display all messages, including the message sent by the HeroService when it fetches heroes.

  1. Add a new Razor component to the Pages folder and name it Messages.razor.
  2. Open Messages component and inject the MessageService:
    @inject IMessageService messageService
    
  3. Replace the rest of the code of the Messages component with the following:
    @if (messageService.Messages.Any())
    {
        <div class="messages">
    
            <h3>Messages</h3>
            <button class="clear" @onclick="@(e => messageService.Clear())">
                clear
            </button>
            @foreach (var message in messageService.Messages)
            {
                <div> @message</div>
            }
        </div>
    }
    
    This code uses the same if and foreach statements that you used in the Heroes component to conditionally display content and iterate over collections.
  4. Add the following styles to site.css:
    .messages{
        clear: both;
    }
    input[text], button {
        color: crimson;
        font-family: Cambria, Georgia;
    }
    
    button {
        background-color: #eee;
        border: none;
        padding: 5px 10px;
        border-radius: 4px;
        cursor: pointer;
        cursor: hand;
        font-family: Arial;
    }
    
    button.clear {
        font-family: Arial;
        background-color: #eee;
        border: none;
        padding: 5px 10px;
        border-radius: 4px;
        cursor: pointer;
        cursor: hand;
    }
    
    button:hover {
        background-color: #cfd8dc;
    }
    
    button:disabled {
        background-color: #eee;
        color: #aaa;
        cursor: auto;
    }
    
    button.clear {
        color: #333;
        margin-bottom: 12px;
    }
    
  5. Amend the Heroes component to include the Messages component:
    @page "/heroes"
    @inject IHeroService  heroService
    
    <h2>My Heroes</h2>
    <ul class="heroes">
        @foreach (var hero in heroes)
        {
            <li @onclick="@(e => onSelect(hero))" class="@(hero == selectedHero ? "selected" : "")">
                <span class="badge">@hero.Id</span> @hero.Name
            </li>
        }
    </ul>
    
    <HeroDetail hero="@selectedHero"></HeroDetail>
    <Messages />
    

Run the application and navigate to /heroes. The page displays the list of heroes. Scroll to the bottom to see the message from the HeroService in the message area. Click the "clear" button and the message area disappears:

Message Service

Summary

  • You defined the behaviour of the HeroService in an interface
  • You refactored data access to the HeroService class, ensuring that it implemented the interface.
  • You registered the HeroService as the provider of its service so that it can be injected anywhere in the app.
  • You used @inject to inject it into a component.
  • The component's OnInitializedAsync lifecycle hook calls the HeroService method.
  • You created a MessageService for loosely-coupled communication between classes.
  • The HeroService injected into a component is created with another injected service, MessageService.

Next: Routing

Last updated: 15/02/2023 09:04:47

Latest Updates

© 2023 - 2024 - Mike Brind.
All rights reserved.
Contact me at Outlook.com