Module 8 - Add API endpoints

In this module, you will substitute the mock service we created in module 3 with a service that interacts with real search results coming from YouTube.

For this module you'll need the Google API key you've obtained in the first module.

Obtain results from YouTube API

The Uno Platform HTTP extension and Refit

To interact with the remote endpoints you will utilize the Uno Platform HTTP extension using Refit. This extension enables registering HTTP API endpoints with the Dependency Injection service collection. The endpoints can then be consumed from the service provider ready to use with pre-configured HttpClients.
Refit takes it a step further and enables you to add attributes to your API endpoint contract interface methods, that contain instructions on how each method in the interface relates to a remote HTTP URL. When such an interface is requested from the DI service provider, it is automatically materialized with the instructions given via the Refit attributes, using the registered HttpClient configuration.

To learn more about these extensions, refer to the HTTP extension docs.

Add Http Feature

You can set up the Http UnoFeature in a few ways. During project setup, pick 'Http' under 'Extensions' in the wizard. Or, use the CLI with the right parameter during project creation. Also you can add it manually later in the project file.

Here's how to do it now:

  1. Open the TubePlayer.csproj file.
  2. Locate the "UnoFeatures" property within the "PropertyGroup" section.
  3. Add "Http" to the list of features, as shown in the snippet below:
    <UnoFeatures>
      <!-- Other features here-->
+     Http; 
    </UnoFeatures>

Add necessary models

Open the file ServicesModelsModels.cs you've previously edited and appended the following content to it:


public partial record IdData(string? VideoId);

public partial record YoutubeVideoData(IdData? Id, SnippetData? Snippet);

public record PageInfoData(int? TotalResults, int? ResultsPerPage);

public record VideoSearchResultData(IImmutableList<YoutubeVideoData>? Items, string? NextPageToken, PageInfoData? PageInfo);

Add Refit namespace

Add the following namespace to the GlobalUsings.cs file:

global using Refit;
global using TubePlayer.Services;

Create video search API endpoint

In the Services folder add a file called IYoutubeEndpoint.cs and replace its contents with the following:

IYoutubeEndpoint.cs code contents (collapsed for brevity)
namespace TubePlayer.Services;

[Headers("Content-Type: application/json")]
public interface IYoutubeEndpoint
{
    [Get($"/search?part=snippet&maxResults={{maxResult}}&type=video&q={{searchQuery}}&pageToken={{nextPageToken}}")]
    [Headers("Authorization: Bearer")]
    Task<VideoSearchResultData?> SearchVideos(string searchQuery, string nextPageToken , uint maxResult, CancellationToken ct);

    [Get($"/channels?part=snippet,statistics")]
    [Headers("Authorization: Bearer")]
    Task<ChannelSearchResultData?> GetChannels([Query(CollectionFormat.Multi)] string[] id, CancellationToken ct );

    [Get($"/videos?part=contentDetails,id,snippet,statistics")]
    [Headers("Authorization: Bearer")]
    Task<VideoDetailsResultData?> GetVideoDetails([Query(CollectionFormat.Multi)] string[] id, CancellationToken ct );
}

The attributes in this interface are the Refit instructions on how to interact with the API. In the following step, you will set up Refit with the DI container.

Create a class to hold the endpoint Refit options

Add another class (in the Services folder) called YoutubeEndpointOptions.cs with the following content:

namespace TubePlayer.Services;

public class YoutubeEndpointOptions : EndpointOptions
{
    public string? ApiKey { get; init; }
}

Add another implementation of IYoutubeService

In the Business folder add a file named YoutubeService.cs with the following content:

YoutubeService.cs code contents (collapsed for brevity)
namespace TubePlayer.Business;

public class YoutubeService(IYoutubeEndpoint client) : IYoutubeService
{
    public async Task<YoutubeVideoSet> SearchVideos(string searchQuery, string nextPageToken, uint maxResult, CancellationToken ct)
    {
        var resultData = await client.SearchVideos(searchQuery, nextPageToken, maxResult, ct);

        var results = resultData?.Items?.Where(result =>
            !string.IsNullOrWhiteSpace(result.Snippet?.ChannelId)
            && !string.IsNullOrWhiteSpace(result.Id?.VideoId))
            .ToArray();

        if (results?.Any() is not true)
        {
            return YoutubeVideoSet.CreateEmpty();
        }

        var channelIds = results!
            .Select(v => v.Snippet!.ChannelId!)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        var videoIds = results!
            .Select(v => v.Id!.VideoId!)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        var asyncDetails = client.GetVideoDetails(videoIds, ct);
        var asyncChannels = client.GetChannels(channelIds, ct);
        await Task.WhenAll(asyncDetails, asyncChannels);

        var detailsItems = (await asyncDetails)?.Items;
        var channelsItems = (await asyncChannels)?.Items;

        if (detailsItems is null || channelsItems is null)
        {
            return YoutubeVideoSet.CreateEmpty();
        }

        var detailsResult = detailsItems!
            .Where(detail => !string.IsNullOrWhiteSpace(detail.Id))
            .DistinctBy(detail => detail.Id)
            .ToDictionary(detail => detail.Id!, StringComparer.OrdinalIgnoreCase);

        var channelsResult = channelsItems!
            .Where(channel => !string.IsNullOrWhiteSpace(channel.Id))
            .DistinctBy(channel => channel.Id)
            .ToDictionary(channel => channel.Id!, StringComparer.OrdinalIgnoreCase);

        var videoSet = new List<YoutubeVideo>();
        foreach (var result in results)
        {
            if (channelsResult.TryGetValue(result.Snippet!.ChannelId!, out var channel)
                && detailsResult.TryGetValue(result.Id!.VideoId!, out var details))
            {
                videoSet.Add(new YoutubeVideo(channel, details));
            }
        }

        return new(videoSet.ToImmutableList(), resultData?.NextPageToken ?? string.Empty);
    }
}

Add app settings

  1. Under the project, open the file appsettings.development.json (cascaded under appsettings.json in Visual Studio Solution Explorer) and replace its contents with the following:

    {
        "AppConfig": {
            "Title": "TubePlayer"
        },
        "YoutubeEndpoint": {
            "Url": "https://youtube.googleapis.com/youtube/v3",
            "ApiKey": "your_development_api_key",
            "UseNativeHandler": true
        },
        "YoutubePlayerEndpoint": {
            "Url": "https://www.youtube.com/youtubei/v1",
            "UseNativeHandler": true
        }
    }
    

    These settings are loaded as part of the app configuration. Read more at the Uno Platform Configuration overview.

  2. You can spot the ApiKey setting above, and replace its value (your_development_api_key) with the API key you obtained from Google API in Module 1.

Register services

  1. Let's instruct our app to use HTTP and tell it about the Refit client. In the App.xaml.cs file, add the following section after the UseSerialization call's closing parentheses:

    .UseHttp(configure: (context, services) =>
    {
        services.AddRefitClientWithEndpoint<IYoutubeEndpoint, YoutubeEndpointOptions>(
            context,
            configure: (clientBuilder, options) => clientBuilder
                .ConfigureHttpClient(httpClient =>
                {
                    httpClient.BaseAddress = new Uri(options!.Url!);
                    httpClient.DefaultRequestHeaders.Add("x-goog-api-key", options.ApiKey);
                }));
    })
    
  2. As you can see, there is no actual implementation of IYoutubeEndpoint, Refit takes care of that and provides a proxy class with all functionality needed, based on the attributes provided on the interface.

    The YoutubeEndpointOptions are automatically materialized with the values under the YoutubeEndpoint key in the appsettings.json file.
    Read this if you want to learn more about using Google API keys in web requests from client libraries.

    Tip

    There are additional overloads to the AddRefitClient extension, the one above uses the AddRefitClientWithEndpoint variation. This is necessary as we need to configure the HttpClient with an additional header containing the API key for YouTube to authorize the request and respond with search results. You'll use the AddRefitClient overload in module 9 - add media player.

  3. Replace the service registration for YoutubeServiceMock we added earlier with the new YoutubeService we just created:

                    .ConfigureServices((context, services) =>
                    {
                        // Register your services
    #if USE_MOCKS
                        services.AddSingleton<IYoutubeService, YoutubeServiceMock>();
    #else
                        services.AddSingleton<IYoutubeService, YoutubeService>();
    #endif
                    })
    

    The USE_MOCKS pre-processor directive allows you to control whether you want to run the app using the mock service or the one we've just implemented.

Update feed to support pagination

  1. Replace the VideoSearchResults feed in MainModel.cs with this one, which supports pagination by cursor:

    public IListFeed<YoutubeVideo> VideoSearchResults => SearchTerm
        .Where(searchTerm => searchTerm is { Length: > 0 })
        .SelectPaginatedByCursorAsync(
            firstPage: string.Empty,
            getPage: async (searchTerm, nextPageToken, desiredPageSize, ct) =>
            {
                var videoSet = await YoutubeService.SearchVideos(searchTerm, nextPageToken, desiredPageSize ?? 10, ct);
    
                return new PageResult<string, YoutubeVideo>(videoSet.Videos, videoSet.NextPageToken);
            });
    

    Read more about MVUX pagination here.

  2. Import this namespace if it has not been added automatically already:

    using Uno.Extensions.Reactive.Sources;
    

Run the app

  1. When you run the app, you will now see that the results are coming from YouTube.

  2. Feel free to change around the search term to see it updating.

  3. Scroll down to load additional infinite results from YouTube.

  4. Try several searches to see how the app displays search results from YouTube. However, if you clear the search box, an empty screen will show up, whereas we'd instead want a pre-designed template to indicate that.

    Screen recording of actual YouTube search results

  5. In addition, try switching off the internet access of the debugging device (for example, if you're using an Android Emulator turn on flight mode), then perform a search:

    Screen recording of the blank screen when searching YouTube while on flight-mode

Next step

In the next module, you'll learn how to utilize the FeedView control and customize its templates to adapt to such scenarios.

Previous | Next