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 HttpClient
s.
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:
- Open the TubePlayer.csproj file.
- Locate the "UnoFeatures" property within the "PropertyGroup" section.
- 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 Services → Models → Models.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
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.
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
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); })); })
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 theYoutubeEndpoint
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 theAddRefitClientWithEndpoint
variation. This is necessary as we need to configure theHttpClient
with an additional header containing the API key for YouTube to authorize the request and respond with search results. You'll use theAddRefitClient
overload in module 9 - add media player.Replace the service registration for
YoutubeServiceMock
we added earlier with the newYoutubeService
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
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.
Import this namespace if it has not been added automatically already:
using Uno.Extensions.Reactive.Sources;
Run the app
When you run the app, you will now see that the results are coming from YouTube.
Feel free to change around the search term to see it updating.
Scroll down to load additional infinite results from YouTube.
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.
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:
Next step
In the next module, you'll learn how to utilize the FeedView
control and customize its templates to adapt to such scenarios.