Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

moved setting the base url and the management key header to a common place for all consumers #20

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Mobile/ContosoFieldService.Core/Services/BaseAPIService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Net;
using System.Net.Http;
using System.Threading;
using ContosoFieldService.Helpers;
using Polly;
using Refit;
using MonkeyCache.FileStore;
Expand Down Expand Up @@ -50,6 +51,20 @@ protected BaseAPIService()
});
}

protected TService GetManagedApiService<TService>()
{
// Here we set up the stuff that is the same in each http request for our api.
// In this case it's the base url and the api management key header.
var client = new HttpClient
{
BaseAddress = new Uri(Constants.BaseUrl)
};

client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", Constants.ApiManagementKey);

return RestService.For<TService>(client);
}

public void InvalidateCache(string key = "")
{
if (string.IsNullOrEmpty(key))
Expand Down
45 changes: 21 additions & 24 deletions Mobile/ContosoFieldService.Core/Services/JobsAPIService.cs
Original file line number Diff line number Diff line change
@@ -1,46 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using ContosoFieldService.Models;
using Polly;
using Refit;
using ContosoFieldService.Helpers;
using MonkeyCache.FileStore;
using Xamarin.Essentials;
using Microsoft.AppCenter.Crashes;

using Microsoft.AppCenter.Crashes;
namespace ContosoFieldService.Services
{
public interface IJobServiceAPI
{
[Get("/job/")]
Task<List<Job>> GetJobs([Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<List<Job>> GetJobs();

[Get("/job/{id}/")]
Task<Job> GetJobById(string id, [Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<Job> GetJobById(string id);

[Get("/jobs?keyword={keyword}&suggestions={enableSuggestions}")]
Task<List<Job>> SearchJobs(string keyword, bool enableSuggestions, [Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<List<Job>> SearchJobs(string keyword, bool enableSuggestions);

[Post("/job/")]
Task<Job> CreateJob([Body] Job job, [Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<Job> CreateJob([Body] Job job);

[Delete("/job/{id}/")]
Task<Job> DeleteJob(string id, [Header("Authorization")] string authorization, [Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<Job> DeleteJob(string id, [Header("Authorization")] string authorization);

[Put("/job/{id}/")]
Task<Job> UpdateJob(string id, [Body] Job job, [Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<Job> UpdateJob(string id, [Body] Job job);
}

public class JobsAPIService : BaseAPIService
{
// Note: Usually, we would create only one instance of the IJobServiceAPI here and
// Re-use it for every operation. For this demo, we can change the BaseUrl at runtime, so we
// Need a way to create the api for every single call
// IJobServiceAPI api = RestService.For<IJobServiceAPI>(Constants.BaseUrl);
// IJobServiceAPI api = GetManagedApiService<IJobServiceAPI>();

public JobsAPIService()
{
Expand All @@ -66,10 +63,10 @@ public JobsAPIService()
try
{
// Create an instance of the Refit RestService for the job interface.
IJobServiceAPI api = RestService.For<IJobServiceAPI>(Constants.BaseUrl);
IJobServiceAPI api = GetManagedApiService<IJobServiceAPI>();

// Use Polly to handle retrying
var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.GetJobs(Constants.ApiManagementKey));
var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.GetJobs());
if (pollyResult.Result != null)
{
// Save jobs into the cache
Expand Down Expand Up @@ -108,9 +105,9 @@ public JobsAPIService()
public async Task<(ResponseCode code, Job result)> GetJobByIdAsync(string id)
{
// Create an instance of the Refit RestService for the job interface.
IJobServiceAPI api = RestService.For<IJobServiceAPI>(Constants.BaseUrl);
IJobServiceAPI api = GetManagedApiService<IJobServiceAPI>();

var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.GetJobById(id, Constants.ApiManagementKey));
var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.GetJobById(id));
if (pollyResult.Result != null)
{
return (ResponseCode.Success, pollyResult.Result);
Expand All @@ -122,9 +119,9 @@ public JobsAPIService()
public async Task<(ResponseCode code, List<Job> result)> SearchJobsAsync(string keyword, bool enableSuggestions = false)
{
// Create an instance of the Refit RestService for the job interface.
IJobServiceAPI api = RestService.For<IJobServiceAPI>(Constants.BaseUrl);
IJobServiceAPI api = GetManagedApiService<IJobServiceAPI>();

var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.SearchJobs(keyword, enableSuggestions, Constants.ApiManagementKey));
var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.SearchJobs(keyword, enableSuggestions));
if (pollyResult.Result != null)
{
return (ResponseCode.Success, pollyResult.Result);
Expand All @@ -136,9 +133,9 @@ public JobsAPIService()
public async Task<(ResponseCode code, Job result)> CreateJobAsync(Job job)
{
// Create an instance of the Refit RestService for the job interface.
IJobServiceAPI api = RestService.For<IJobServiceAPI>(Constants.BaseUrl);
IJobServiceAPI api = GetManagedApiService<IJobServiceAPI>();

var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.CreateJob(job, Constants.ApiManagementKey));
var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.CreateJob(job));
if (pollyResult.Result != null)
{
return (ResponseCode.Success, pollyResult.Result);
Expand All @@ -150,9 +147,9 @@ public JobsAPIService()
public async Task<(ResponseCode code, Job result)> DeleteJobByIdAsync(string id)
{
// Create an instance of the Refit RestService for the job interface.
IJobServiceAPI api = RestService.For<IJobServiceAPI>(Constants.BaseUrl);
IJobServiceAPI api = GetManagedApiService<IJobServiceAPI>();

var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.DeleteJob(id, "Bearer " + AuthenticationService.AccessToken, Constants.ApiManagementKey));
var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.DeleteJob(id, "Bearer " + AuthenticationService.AccessToken));
if (pollyResult.Result != null)
{
return (ResponseCode.Success, pollyResult.Result);
Expand All @@ -164,9 +161,9 @@ public JobsAPIService()
public async Task<(ResponseCode code, Job result)> UpdateJob(Job job)
{
// Create an instance of the Refit RestService for the job interface.
IJobServiceAPI api = RestService.For<IJobServiceAPI>(Constants.BaseUrl);
IJobServiceAPI api = GetManagedApiService<IJobServiceAPI>();

var results = await api.UpdateJob(job.Id, job, Constants.ApiManagementKey);
var results = await api.UpdateJob(job.Id, job);
if (results != null)
{
return (ResponseCode.Success, results);
Expand Down
24 changes: 12 additions & 12 deletions Mobile/ContosoFieldService.Core/Services/PartsAPIService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,34 @@
using System.Linq;
using System;
using Xamarin.Essentials;
using Microsoft.AppCenter.Crashes;

using Microsoft.AppCenter.Crashes;
namespace ContosoFieldService.Services
{
public interface IPartsServiceAPI
{
[Get("/part/")]
Task<List<Part>> GetParts([Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<List<Part>> GetParts();

[Get("/part/{id}/")]
Task<Part> GetPartById(string id, [Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<Part> GetPartById(string id);

[Get("/search/parts/?keyword={keyword}")]
Task<List<Part>> SearchParts(string keyword, [Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<List<Part>> SearchParts(string keyword);

[Post("/part/")]
Task<Part> CreatePart([Body] Part part, [Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<Part> CreatePart([Body] Part part);

[Post("/part/{id}/")]
Task<Part> DeletePart(string id, [Header("Authorization")] string authorization, [Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<Part> DeletePart(string id, [Header("Authorization")] string authorization);
}

public class PartsAPIService : BaseAPIService
{
// Note: Usually, we would create only one instance of the IPartsServiceAPI here and
// Re-use it for every operation. For this demo, we can chance the BaseUrl at runtime, so we
// Need a way to create the api for every single call
// readonly IPartsServiceAPI api = RestService.For<IPartsServiceAPI>(Helpers.Constants.BaseUrl);
// readonly IPartsServiceAPI api = GetManagedApiService<IPartsServiceAPI>();

public PartsAPIService()
{
Expand All @@ -59,10 +59,10 @@ public PartsAPIService()

try
{
IPartsServiceAPI api = RestService.For<IPartsServiceAPI>(Helpers.Constants.BaseUrl);
IPartsServiceAPI api = GetManagedApiService<IPartsServiceAPI>();

// Use Polly to handle retrying
var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.GetParts(Constants.ApiManagementKey));
var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.GetParts());
if (pollyResult.Result != null)
{
// Save parts into the cache
Expand Down Expand Up @@ -94,9 +94,9 @@ public PartsAPIService()

public async Task<(ResponseCode code, List<Part> result)> SearchPartsAsync(string keyword)
{
IPartsServiceAPI api = RestService.For<IPartsServiceAPI>(Helpers.Constants.BaseUrl);
IPartsServiceAPI api = GetManagedApiService<IPartsServiceAPI>();

var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.SearchParts(keyword, Constants.ApiManagementKey));
var pollyResult = await Policy.ExecuteAndCaptureAsync(async () => await api.SearchParts(keyword));
if (pollyResult.Result != null)
{
return (ResponseCode.Success, pollyResult.Result);
Expand Down
9 changes: 4 additions & 5 deletions Mobile/ContosoFieldService.Core/Services/PhotoAPIService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,24 @@
using ContosoFieldService.Models;
using Plugin.Media.Abstractions;
using Refit;
using ContosoFieldService.Helpers;

namespace ContosoFieldService.Services
{
public interface IPhotoServiceAPI
{
[Multipart]
[Post("/photo/{jobId}/")]
Task<Job> UploadPhoto(string jobId, [AliasAs("file")] StreamPart stream, [Header("Ocp-Apim-Subscription-Key")] string apiManagementKey);
Task<Job> UploadPhoto(string jobId, [AliasAs("file")] StreamPart stream);
}

public class PhotoAPIService
public class PhotoAPIService : BaseAPIService
{
public async Task<Job> UploadPhotoAsync(string jobId, MediaFile file)
{
IPhotoServiceAPI api = RestService.For<IPhotoServiceAPI>(Constants.BaseUrl);
IPhotoServiceAPI api = GetManagedApiService<IPhotoServiceAPI>();

var streamPart = new StreamPart(file.GetStream(), "photo.jpg", "image/jpeg");
var updatedJob = await api.UploadPhoto(jobId, streamPart, Constants.ApiManagementKey);
var updatedJob = await api.UploadPhoto(jobId, streamPart);
return updatedJob;
}
}
Expand Down
17 changes: 14 additions & 3 deletions Walkthrough Guide/09 Mobile Network Services/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,25 @@ It requires us to define our REST API calls as a C# Interface which is then used


#### Security
Because we’re using Azure API Management, we have the ability to restrict access to our APIs through the use of API Keys. With a unique API key, we’re able to confirm that this app is allowed access to our services. We’ll want to ensure we add the API key to all our requests and Refit makes this super easy! We just need to add the _Headers_ attribute to our interface. Here we grab our API key from the constants class.
Because we’re using Azure API Management, we have the ability to restrict access to our APIs through the use of API Keys. With a unique API key, we’re able to confirm that this app is allowed access to our services. We’ll want to ensure we add the API key to all our requests and Refit makes this super easy! We just need to set the `Ocp-Apim-Subscription-Key` header in every request. Here we grab our API key from the constants class and add it to the default headers for the created services.


```cs
[Headers(Helpers.Constants.ApiManagementKey)]`
public interface IJobServiceAPI`
public abstract class BaseAPIService
{
protected TService GetManagedApiService<TService>()
{
// Here we set up the stuff that is the same in each http request for our api.
// In this case it's the base url and the api management key header.
var client = new HttpClient
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we possibly reuse the HTTP Client if we've already created one? Currently we'd be creating a new client for each request.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can. I thought about it, but didn't want to introduce side effects regarding DNS cache stuff. If you wan't just add Lazy<HttpClient> :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we we're originally new-ing up an instance of HttpClient for every request so your change isn't breaking the existing solution but rather brought to my attention to something we could improve.

I'm guessing the DNS caching side effect you mention is related to the HttpClient instance holding DNS info for 2 minutes? If thats the case then we should* be safe given we use API Management in production and the DNS is locked down and not changing but with that said, I think we can make this all work nicely...

One vs multiple clients
To quote the docs on HttpClient:

HttpClient is intended to be instantiated once and re-used throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads. This will result in SocketException errors.

Sockets can be held for up to 4 minutes per request so this might actually be causing our network stability issues when we send too many requests. I'd love @robinmanuelthiel to share his thoughts on this as well.

Possible solution to DNS Cache with 1 instance
We could use set the DnsRefreshTimeout property of ServicePointManager class to set our own DNS refresh timeout to something more sensible (if we REALLY needed to). This could help mitigate any issues with DNS caching.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't get me wrong. I'm pro caching, too. I just did not want to change the current behavior :P

{
BaseAddress = new Uri(Constants.BaseUrl)
};

client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", Constants.ApiManagementKey);

return RestService.For<TService>(client);
}
}

```
Expand Down