How to mock Microsoft Graph API SDK Client? - c#

I have used Microsoft Graph SDK in my project to call graph API, for this I need to use GraphServiceClient.
To use GraphServiceClient, i have to add some helper classes, in which SDKHelper is a static class which has GetAuthenticatedClient() method.
Since method under test is tightly coupled to SDKHelper which is static, so I have created a service class and injected the dependency.
Below is the controller and method,
public class MyController
{
private IMyServices _iMyServices { get; set; }
public UserController(IMyServices iMyServices)
{
_iMyServices = iMyServices;
}
public async Task<HttpResponseMessage> GetGroupMembers([FromUri]string groupID)
{
GraphServiceClient graphClient = _iMyServices.GetAuthenticatedClient();
IGroupMembersCollectionWithReferencesPage groupMembers = await _iMyServices.GetGroupMembersCollectionWithReferencePage(graphClient, groupID);
return this.Request.CreateResponse(HttpStatusCode.OK, groupMembers, "application/json");
}
}
Service Class,
public class MyServices : IMyServices
{
public GraphServiceClient GetAuthenticatedClient()
{
GraphServiceClient graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
string accessToken = await SampleAuthProvider.Instance.GetAccessTokenAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
}));
return graphClient;
}
public async Task<IGraphServiceGroupsCollectionPage> GetGraphServiceGroupCollectionPage(GraphServiceClient graphClient)
{
return await graphClient.Groups.Request().GetAsync();
}
}
I am having challenge in writing Unit Test Case for the above service class methods, Below is my Unit Test Code:
public async Task GetGroupMembersCollectionWithReferencePage_Success()
{
GraphServiceClient graphClient = GetAuthenticatedClient();
IGraphServiceGroupsCollectionPage groupMembers = await graphClient.Groups.Request().GetAsync();
Mock<IUserServices> mockIUserService = new Mock<IUserServices>();
IGraphServiceGroupsCollectionPage expectedResult = await mockIUserService.Object.GetGraphServiceGroupCollectionPage(graphClient);
Assert.AreEqual(expectedResult, groupMembers);
}
In Above Test case line number 4 throws an exception -
Message: The type initializer for 'Connect3W.UserMgt.Api.Helpers.SampleAuthProvider' threw an exception.
Inner Exception Message: Value cannot be null. Parameter name: format
Can anyone suggest me how to use MOQ to mock above code or any other method to complete test case for this ?

Do not mock what you do not own. GraphServiceClient should be treated as a 3rd party dependency and should be encapsulated behind abstractions you control
You attempted to do that but are still leaking implementation concerns.
The service can be simplified to
public interface IUserServices {
Task<IGroupMembersCollectionWithReferencesPage> GetGroupMembers(string groupID);
}
and the implementation
public class UserServices : IUserServices {
GraphServiceClient GetAuthenticatedClient() {
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
string accessToken = await SampleAuthProvider.Instance.GetAccessTokenAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
}));
return graphClient;
}
public Task<IGroupMembersCollectionWithReferencesPage> GetGroupMembers(string groupID) {
var graphClient = GetAuthenticatedClient();
return graphClient.Groups[groupID].Members.Request().GetAsync();
}
}
Which would result in the controller being simplified as well
public class UserController : ApiController {
private readonly IUserServices service;
public UserController(IUserServices myServices) {
this.service = myServices;
}
public async Task<IHttpActionResult> GetGroupMembers([FromUri]string groupID) {
IGroupMembersCollectionWithReferencesPage groupMembers = await service.GetGroupMembers(groupID);
return Ok(groupMembers);
}
}
Now for testing of the controller you can easily mock the abstractions to behave as expected in order to exercise the test to completion because the controller is completely decoupled from the GraphServiceClient 3rd party dependency and the controller can be tested in isolation.
[TestClass]
public class UserControllerShould {
[TestMethod]
public async Task GetGroupMembersCollectionWithReferencePage_Success() {
//Arrange
var groupId = "12345";
var expectedResult = Mock.Of<IGroupMembersCollectionWithReferencesPage>();
var mockService = new Mock<IUserServices>();
mockService
.Setup(_ => _.GetGroupMembers(groupId))
.ReturnsAsync(expectedResult);
var controller = new UserController(mockService.Object);
//Act
var result = await controller.GetGroupMembers(groupId) as System.Web.Http.Results.OkNegotiatedContentResult<IGroupMembersCollectionWithReferencesPage>;
//Assert
Assert.IsNotNull(result);
var actualResult = result.Content;
Assert.AreEqual(expectedResult, actualResult);
}
}

An alternative solution to #Nkosi. Using the constructor public GraphServiceClient(IAuthenticationProvider authenticationProvider, IHttpProvider httpProvider = null); we can Mock the requests actually made.
Complete example below.
Our GraphApiService uses IMemoryCache, to cache both AccessToken and Users from ADB2C, IHttpClientFactory for HTTP requests and Settings is from appsettings.json.
https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-5.0
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0
public class GraphApiService
{
private readonly IHttpClientFactory _clientFactory;
private readonly IMemoryCache _memoryCache;
private readonly Settings _settings;
private readonly string _accessToken;
public GraphApiService(IHttpClientFactory clientFactory, IMemoryCache memoryCache, Settings settings)
{
_clientFactory = clientFactory;
_memoryCache = memoryCache;
_settings = settings;
string graphApiAccessTokenCacheEntry;
// Look for cache key.
if (!_memoryCache.TryGetValue(CacheKeys.GraphApiAccessToken, out graphApiAccessTokenCacheEntry))
{
// Key not in cache, so get data.
var adb2cTokenResponse = GetAccessTokenAsync().GetAwaiter().GetResult();
graphApiAccessTokenCacheEntry = adb2cTokenResponse.access_token;
// Set cache options.
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromSeconds(adb2cTokenResponse.expires_in));
// Save data in cache.
_memoryCache.Set(CacheKeys.GraphApiAccessToken, graphApiAccessTokenCacheEntry, cacheEntryOptions);
}
_accessToken = graphApiAccessTokenCacheEntry;
}
public async Task<List<Adb2cUser>> GetAllUsersAsync(bool refreshCache = false)
{
if (refreshCache)
{
_memoryCache.Remove(CacheKeys.Adb2cUsers);
}
return await _memoryCache.GetOrCreateAsync(CacheKeys.Adb2cUsers, async (entry) =>
{
entry.SetAbsoluteExpiration(TimeSpan.FromHours(1));
var authProvider = new AuthenticationProvider(_accessToken);
GraphServiceClient graphClient = new GraphServiceClient(authProvider, new HttpClientHttpProvider(_clientFactory.CreateClient()));
var users = await graphClient.Users
.Request()
.GetAsync();
return users.Select(user => new Adb2cUser()
{
Id = Guid.Parse(user.Id),
GivenName = user.GivenName,
FamilyName = user.Surname,
}).ToList();
});
}
private async Task<Adb2cTokenResponse> GetAccessTokenAsync()
{
var client = _clientFactory.CreateClient();
var kvpList = new List<KeyValuePair<string, string>>();
kvpList.Add(new KeyValuePair<string, string>("grant_type", "client_credentials"));
kvpList.Add(new KeyValuePair<string, string>("client_id", _settings.AzureAdB2C.ClientId));
kvpList.Add(new KeyValuePair<string, string>("scope", "https://graph.microsoft.com/.default"));
kvpList.Add(new KeyValuePair<string, string>("client_secret", _settings.AzureAdB2C.ClientSecret));
#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
var req = new HttpRequestMessage(HttpMethod.Post, $"https://login.microsoftonline.com/{_settings.AzureAdB2C.Domain}/oauth2/v2.0/token")
{ Content = new FormUrlEncodedContent(kvpList) };
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation
using var httpResponse = await client.SendAsync(req);
var response = await httpResponse.Content.ReadAsStringAsync();
httpResponse.EnsureSuccessStatusCode();
var adb2cTokenResponse = JsonSerializer.Deserialize<Adb2cTokenResponse>(response);
return adb2cTokenResponse;
}
}
public class AuthenticationProvider : IAuthenticationProvider
{
private readonly string _accessToken;
public AuthenticationProvider(string accessToken)
{
_accessToken = accessToken;
}
public Task AuthenticateRequestAsync(HttpRequestMessage request)
{
request.Headers.Add("Authorization", $"Bearer {_accessToken}");
return Task.CompletedTask;
}
}
public class HttpClientHttpProvider : IHttpProvider
{
private readonly HttpClient http;
public HttpClientHttpProvider(HttpClient http)
{
this.http = http;
}
public ISerializer Serializer { get; } = new Serializer();
public TimeSpan OverallTimeout { get; set; } = TimeSpan.FromSeconds(300);
public void Dispose()
{
}
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
{
return http.SendAsync(request);
}
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
HttpCompletionOption completionOption,
CancellationToken cancellationToken)
{
return http.SendAsync(request, completionOption, cancellationToken);
}
}
We then use the GraphApiService in various Controllers. Example from a simple CommentController below. CommentService not included but it is not needed for the example anyway.
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class CommentController : ControllerBase
{
private readonly CommentService _commentService;
private readonly GraphApiService _graphApiService;
public CommentController(CommentService commentService, GraphApiService graphApiService)
{
_commentService = commentService;
_graphApiService = graphApiService;
}
[HttpGet("{rootEntity}/{id}")]
public ActionResult<IEnumerable<CommentDto>> Get(RootEntity rootEntity, int id)
{
var comments = _commentService.Get(rootEntity, id);
var users = _graphApiService.GetAllUsersAsync().GetAwaiter().GetResult();
var commentDtos = new List<CommentDto>();
foreach (var comment in comments)
{
commentDtos.Add(CommonToDtoMapper.MapCommentToCommentDto(comment, users));
}
return Ok(commentDtos);
}
[HttpPost("{rootEntity}/{id}")]
public ActionResult Post(RootEntity rootEntity, int id, [FromBody] string message)
{
_commentService.Add(rootEntity, id, message);
_commentService.SaveChanges();
return Ok();
}
}
Since we use our own IAuthenticationProvider and IHttpProvider we can mock IHttpClientFactory based on what URI is called. Complete test example below, check mockMessageHandler.Protected() to see how the requests are mocked. To find the exact request made we look at the documentation. For example var users = await graphClient.Users.Request().GetAsync(); is equivalent to GET https://graph.microsoft.com/v1.0/users.
https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0&tabs=http#request
public class CommentControllerTest : SeededDatabase
{
[Fact]
public void Get()
{
using (var context = new ApplicationDbContext(_dbContextOptions))
{
var controller = GeCommentController(context);
var result = controller.Get(RootEntity.Question, 1).Result;
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<CommentDto>>(okResult.Value);
Assert.Equal(2, returnValue.Count());
}
}
[Theory]
[MemberData(nameof(PostData))]
public void Post(RootEntity rootEntity, int id, string message)
{
using (var context = new ApplicationDbContext(_dbContextOptions))
{
var controller = GeCommentController(context);
var result = controller.Post(rootEntity, id, message);
var okResult = Assert.IsType<OkResult>(result);
var comment = context.Comments.First(x => x.Text == message);
if(rootEntity == RootEntity.Question)
{
Assert.Equal(comment.QuestionComments.First().QuestionId, id);
}
}
}
public static IEnumerable<object[]> PostData()
{
return new List<object[]>
{
new object[]
{ RootEntity.Question, 1, "Test comment from PostData" }
};
}
private CommentController GeCommentController(ApplicationDbContext dbContext)
{
var userService = new Mock<IUserResolverService>();
userService.Setup(x => x.GetNameIdentifier()).Returns(DbContextSeed.CurrentUser);
var settings = new Settings();
var commentService = new CommentService(new ExtendedApplicationDbContext(dbContext, userService.Object));
var expectedContentGetAccessTokenAsync = #"{
""token_type"": ""Bearer"",
""expires_in"": 3599,
""ext_expires_in"": 3599,
""access_token"": ""123""
}";
var expectedContentGetAllUsersAsync = #"{
""#odata.context"": ""https://graph.microsoft.com/v1.0/$metadata#users"",
""value"": [
{
""businessPhones"": [],
""displayName"": ""Oscar"",
""givenName"": ""Oscar"",
""jobTitle"": null,
""mail"": null,
""mobilePhone"": null,
""officeLocation"": null,
""preferredLanguage"": null,
""surname"": ""Andersson"",
""userPrincipalName"": """ + DbContextSeed.DummyUserExternalId + #"#contoso.onmicrosoft.com"",
""id"":""" + DbContextSeed.DummyUserExternalId + #"""
}
]
}";
var mockFactory = new Mock<IHttpClientFactory>();
var mockMessageHandler = new Mock<HttpMessageHandler>();
mockMessageHandler.Protected()
#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.AbsoluteUri.Contains("https://login.microsoftonline.com/")), ItExpr.IsAny<CancellationToken>())
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(expectedContentGetAccessTokenAsync)
});
mockMessageHandler.Protected()
#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.AbsoluteUri.Contains("https://graph.microsoft.com/")), ItExpr.IsAny<CancellationToken>())
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(expectedContentGetAllUsersAsync)
});
var httpClient = new HttpClient(mockMessageHandler.Object);
mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);
var services = new ServiceCollection();
services.AddMemoryCache();
var serviceProvider = services.BuildServiceProvider();
var memoryCache = serviceProvider.GetService<IMemoryCache>();
var graphService = new GraphApiService(mockFactory.Object, memoryCache, settings);
var controller = new CommentController(commentService, graphService);
return controller;
}
}

Related

.Net Core - Docker Linux memory usage keeps increasing

I am bit frustrated now what's wrong with my code, and I hope you guys can help me with it, so here are the things I have tried.
so I tried making the HttpClient static, and I tried using the IHttpClientFactory.CreateClient() and I even added this on my .csproj
<ServerGarbageCollection>false</ServerGarbageCollection>
Here is the sample code that I have been doing
public class TestController : BaseController
{
private static HttpClient _httpClient = new();
public TestController()
{
}
[HttpGet("bills")]
public async Task<IActionResult> GetBillsPresentment([FromQuery] GetBillPresentmentQuery query)
{
if (!query.AccountNumber.Contains("-"))
query.AccountNumber = FormatAccountNumber(query.AccountNumber);
var billDetails = await GetBillDetail(query.AccountNumber);
if (billDetails == null)
throw new ProviderProcessException(ProviderErrorCode.INVALID_ACCOUNT_NUMBER);
return Ok(new BillPresentmentVm
{
User = new CustomerDto
{
CustomerName = billDetails.Name
},
Billing = new BillingDto
{
AccountNumber = query.AccountNumber,
DueDate = DateTime.Parse(billDetails.LastReadDate).AddMonths(1),
Outstanding = !string.IsNullOrEmpty(billDetails.Arrears) ? decimal.Parse(billDetails.Arrears) : null
}
});
}
private async Task<ResponseModel> GetBillDetail(string accountNumber)
{
try
{
var payload = new { accno = accountNumber };
string json = JsonConvert.SerializeObject(payload);
var buffer = System.Text.Encoding.UTF8.GetBytes(json);
using var byteContent = new ByteArrayContent(buffer);
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = await _httpClient.PostAsync("https://test.com", byteContent);
if (!response.IsSuccessStatusCode)
throw new ProviderProcessException(ProviderErrorCode.BILLING_CYCLE_UNAVAILABLE);
var result = await response.Content.ReadAsStringAsync();
if (result == "Accno not found!") return null;
var data = JsonConvert.DeserializeObject<ResponseModel>(result);
return data;
}
catch (Exception)
{
throw new ProviderProcessException(ProviderErrorCode.BILLING_CYCLE_UNAVAILABLE);
}
}
private static string FormatAccountNumber(string accountNumber)
{
return string.Format("{0:#######-########}", Convert.ToInt64(accountNumber));
}
}
And here's the docker memory usage
The memory usage keeps increasing after a request. Can someone explains me why it is not decreasing?
Thank you very much in advance
I solve this issue using IHttpClientFactory instead of HttpClient.
https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
I use some thing like this and it works fine on large amount of requests per second and it use memory in normal way .
namespace BehsaLoyalty.ApiClient
{
public class ApiRepo : IApiRepo
{
private readonly IHttpClientFactory _HttpClientFactory;
public ApiRepo (IHttpClientFactory httpClientFactory)
{
_HttpClientFactory = httpClientFactory;
}
public async Task<ResponseModel> PostMyObject(Myobject model, CancellationToken cancellationToken)
{
HttpClient httpClient = _HttpClientFactory.CreateClient("ApiDestinationURI");
using HttpRequestMessage request = new(HttpMethod.Post, "/blah/blah");
request.Content = new StringContent(JsonSerializer.Serialize(model));
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken);
response.EnsureSuccessStatusCode();
string createdContent = await response.Content.ReadAsStringAsync();
ResponseModel ResponseReturn = JsonSerializer.Deserialize<ResponseModel>(createdContent);
return ResponseReturn;
}
}
}

Polly FallbackAsync policy with a dynamic action that raises an event so that the request / response can be written to a database

I'm looking for a way to attach a generic Polly fallback policy to my typed HttpClient. My policy gets applied to certain request types as and when required, otherwise it applies a NoOpPolicy. Im interested in the onFallbackAsync Task. I need to save the request/response to a database table, but I'm struggling to figure out how to apply this because I can't see how to inject a dependency. I seem to be missing something obvious, but I don't know what. Can this be done with a delegating handler?
My typed client basically looks like this, and is used for a variety of different API calls:
public class ApiClient : IApiClient
{
private readonly HttpClient _httpClient;
private readonly ILogger _logger;
private HttpRequestMessage _httpRequestMessage;
private HttpContent _httpContent;
private IDictionary<string, string> _queryParameters;
public ApiClient(HttpClient httpClient, ILogger logger)
{
_httpClient = httpClient;
_logger = logger;
_httpRequestMessage = new HttpRequestMessage();
_queryParameters = new Dictionary<string, string>();
}
public void SetUrl(string urlPath)
{
urlPath = urlPath.TrimEnd('/');
_httpClient.BaseAddress = new Uri(urlPath);
}
public void SetQueryParamters(IDictionary<string, string> parameters)
{
_queryParameters = parameters;
}
public void AddQueryParamter(string key, string value)
{
_queryParameters.Add(key, value);
}
public async Task<T> Get<T>()
{
_httpRequestMessage.Method = HttpMethod.Get;
return await EnvokeService<T>();
}
public async Task<T> PostJsonAsync<T>(object body)
{
var settings = new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
};
_httpContent = new JsonContent(JsonConvert.SerializeObject(body, settings));
_httpRequestMessage.Method = HttpMethod.Post;
return await EnvokeService<T>();
}
private async Task<T> EnvokeService<T>()
{
string responseContent = null;
try
{
_httpRequestMessage.Content = _httpContent;
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
var responseMessage = await _httpClient.SendAsync(_httpRequestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
responseContent = await responseMessage.Content.ReadAsStringAsync();
if (responseMessage.IsSuccessStatusCode)
{
T result;
var serializer = new JsonSerializer();
using (var sr = new StreamReader(await responseMessage.Content.ReadAsStreamAsync()))
using (var jsonTextReader = new JsonTextReader(sr))
{
result = serializer.Deserialize<T>(jsonTextReader);
}
_logger.Debug(logMessage);
return result;
}
throw new HttpRequestException($"Error Code: {responseMessage.StatusCode}. Response: {responseContent}");
}
catch (Exception e)
{
_logger.Exception(e, "Error");
throw;
}
}
}
It is setup like this:
public static class ApiHttpClientConfigurator
{
public static void AddApiHttpClient (this IServiceCollection services)
{
IPolicyRegistry<string> registry = services.AddPolicyRegistry();
registry.Add("WaitAndRetryPolicy", ResiliencePolicies.GetHttpPolicy(new TimeSpan(0, 1, 0)));
registry.Add("NoOpPolicy", Policy.NoOpAsync().AsAsyncPolicy<HttpResponseMessage>());
services.AddHttpClient<ApiHttpClient>().AddPolicyHandlerFromRegistry(PolicySelector);
services.AddSingleton<IApiHttpClientFactory, ApiHttpClientFactory>();
}
private static IAsyncPolicy<HttpResponseMessage> PolicySelector(IReadOnlyPolicyRegistry<string> policyRegistry, HttpRequestMessage httpRequestMessage)
{
// if we have a message of type X then apply the policy
if (httpRequestMessage.RequestUri.AbsoluteUri.Contains("/definitely/apply/a/retry/policy"))
{
return policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>("WaitAndRetryPolicy");
}
return policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>("NoOpPolicy");
}
}
Registered as so:
services.AddApiHttpClient();
My policies are defined as such:
public static class ResiliencePolicies
{
public static IAsyncPolicy<HttpResponseMessage> HttpFallBackPolicy
{
get => Policy<HttpResponseMessage>.Handle<Exception>().FallbackAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError), d =>
{
// TODO: publish event
InstanceOfMediatr.Publish(new RequestFailedEvent(request/response/data/here))
Log.Warning($"Fallback: {d.Exception.GetType().Name} {d.Exception.Message}");
return Task.CompletedTask;
});
}
public static IAsyncPolicy<HttpResponseMessage> HttpRetryPolicy
{
get => HttpPolicyExtensions.HandleTransientHttpError()
.Or<TimeoutRejectedException>()
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(retryAttempt * 0.5));
}
public static IAsyncPolicy<HttpResponseMessage> GetHttpTimeoutPolicy(TimeSpan timeSpan) =>
Policy.TimeoutAsync<HttpResponseMessage>(timeSpan);
public static IAsyncPolicy<HttpResponseMessage> GetHttpPolicy(TimeSpan timeout) =>
Policy.WrapAsync(HttpFallBackPolicy, HttpRetryPolicy, GetHttpTimeoutPolicy(timeout));
}
The final part of the puzzle would appear to be, how can I complete the TODO section above? I seem to need a dynamic fallback action, but I'm really not sure how to implement it, or whether or not it is even possible.
Just in case anyone else comes across this problem, there are two ways to solve it depending upon your requirements (using AddPolicyHandlerFromRegistry() or AddPolicyHandler()).
Through IServiceProvider
AddPolicyHandler() has a convenient overload where you can inject IServiceProvider:
.AddPolicyHandler((provider, message) =>
HttpPolicyExtensions
.HandleTransientHttpError()
.FallbackAsync(
fallbackValue: new HttpResponseMessage(HttpStatusCode.InternalServerError),
onFallbackAsync: (result, context) =>
{
var publisher = provider.GetService<IPublisher>();
publisher.Publish(new HttpRequestFallbackEvent());
return Task.CompletedTask;
}))
Through Context
If you are using a PolicyRegistry and AddPolicyHandlerFromRegistry(), then it is easier to use the Context as described here. First extend the Polly's Context:
public static class ContextExtensions
{
private static readonly string PublisherKey = "PublisherKey";
public static Context WithDependencies(this Context context, IPublisher publisher)
{
context[PublisherKey] = publisher;
return context;
}
public static IPublisher GetPublisher(this Context context)
{
if (context.TryGetValue(PublisherKey, out object publisher))
{
return publisher as IPublisher;
}
return null;
}
}
Then in your client, inject your dependency (i.e. IPublisher) and add it to the new Context and add that to the executing context:
var context = new Context().WithDependencies(_publisher);
request.SetPolicyExecutionContext(context);
var responseMessage = await _httpClient.SendAsync(request)
Now you can use that in your registered and selected policy:
private static IAsyncPolicy<HttpResponseMessage> FallbackPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<Exception>()
.FallbackAsync(
fallbackValue: new HttpResponseMessage(HttpStatusCode.InternalServerError),
onFallbackAsync: (result, context) =>
{
var publisher = context.GetPublisher();
publisher.Publish(new HttpRequestFallbackEvent());
return Task.CompletedTask;
}));
}

Unable to resolve service for type 'System.Net.Http.HttpClient' using DI

I writing phone verification functional via Twilio and using System.Net.Http.HttpClient
I inject it in AppService like this
public class TwilioVerifyClientAppService: IVerifyPhone
{
private readonly HttpClient _client;
public TwilioVerifyClientAppService(HttpClient client)
{
_client = client;
}
public async Task<TwilioSendVerificationCodeResponse> StartVerification(int countryCode, string phoneNumber)
{
var requestContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("via", "sms"),
new KeyValuePair<string, string>("country_code", countryCode.ToString()),
new KeyValuePair<string, string>("phone_number", phoneNumber),
});
var response = await _client.PostAsync("protected/json/phones/verification/start", requestContent);
var content = await response.Content.ReadAsStringAsync();
// this will throw if the response is not valid
return JsonConvert.DeserializeObject<TwilioSendVerificationCodeResponse>(content);
}
public async Task<TwilioCheckCodeResponse> CheckVerificationCode(int countryCode, string phoneNumber,
string verificationCode)
{
var queryParams = new Dictionary<string, string>()
{
{"country_code", countryCode.ToString()},
{"phone_number", phoneNumber},
{"verification_code", verificationCode},
};
var url = QueryHelpers.AddQueryString("protected/json/phones/verification/check", queryParams);
var response = await _client.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
// this will throw if the response is not valid
return JsonConvert.DeserializeObject<TwilioCheckCodeResponse>(content);
}
}
}
When I try to run method, I get this error
Here is how I call this method in controller
[ApiController]
[Route("api/[controller]/[action]")]
public class ProfileController : ControllerBase
{
private readonly IUserProfile _profileAppService;
private readonly UserManager<AppUser> _userManager;
private readonly IFileUpload _fileUpload;
private readonly IVerifyPhone _verifyPhone;
public ProfileController(IUserProfile profileAppService,
UserManager<AppUser> userManager, IFileUpload fileUpload, IVerifyPhone verifyPhone)
{
_profileAppService = profileAppService;
_userManager = userManager;
_fileUpload = fileUpload;
_verifyPhone = verifyPhone;
}
[Authorize]
[HttpPost]
public async Task<IActionResult> ConfirmCodeSend([FromForm] PhoneInputDto input)
{
var result = await _verifyPhone.StartVerification(input.DialingCode, input.PhoneNumber);
if (result.Success)
{
return Ok("Code sent");
}
return BadRequest();
}
[Authorize]
[HttpPost]
public async Task<IActionResult> ConfirmCodeCheck([FromForm] PhoneInputDto input)
{
var result =
await _verifyPhone.CheckVerificationCode(input.DialingCode, input.PhoneNumber, input.VerificationCode);
if (result.Success)
{
return Ok("Phone verified");
}
return BadRequest();
}
}
}
An unhandled exception occurred while processing the request. InvalidOperationException: Unable to resolve service for type
'System.Net.Http.HttpClient' while attempting to activate
'TooSeeWeb.Infrastructure.AppServices.UserProfile.TwilioVerifyClientAppService'.
In Startup.cs
I register my interface like this
services.AddScoped<IVerifyPhone, TwilioVerifyClientAppService>();
I tried to write this in Startup.cs
services.AddHttpClient<TwilioVerifyClientAppService>();
But I still see this error.
How I can solve this?
You registered the class as a typed client but not the interface, yet try to inject the interface as a dependency into the controller.
Update the typed client registration to include the interface
services.AddHttpClient<IVerifyPhone, TwilioVerifyClientAppService>();
provided TwilioVerifyClientAppService is derived from IVerifyPhone
public class TwilioVerifyClientAppService: IVerifyPhone {
//...
}
and remove the scoped registration
services.AddScoped<IVerifyPhone, TwilioVerifyClientAppService>();

Testing controller with xUnit

I have webapi where it needs to call some other endpoint and get data.
My current code as follows
//http client implementation
public interface IHttpClientFactory
{
HttpClient Create();
}
public class HttpClientFactory : IHttpClientFactory
{
private readonly ApplicationSettings _applicationSettings;
HttpClient _httpClient;
public HttpClientFactory(IOptions<ApplicationSettings> settings)
{
_applicationSettings = settings.Value;
}
public HttpClient Create()
{
if (_httpClient != null)
return _httpClient;
var client = new HttpClient()
{
BaseAddress = new Uri($"{_applicationSettings.BaseUrl}")
};
_httpClient = client;
return _httpClient;
}
}
public interface IGetItemsQuery
{
Task<IEnumerable<T>> Execute<T>(string url);
}
public class GetItemQuery: IGetItemsQuery
{
private readonly IHttpClientFactory _httpClientFactory;
public GetPhotosQuery(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<IEnumerable<T>> Execute<T>(string url)
{
using (var response = await _httpClientFactory.Create().GetAsync($"{url}").ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();
var resp = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var items = JArray.Parse(resp);
return items.ToObject<T[]>();
}
}
In my controller part
private readonly IGetItemsQuery _getItemsQuery;
public HomeController(IGetItemsQuery getItemsQuery)
{
_getItemsQuery = getItemsQuery;
}
appsettings
"ApplicationSettings": {
"BaseUrl": "http://someendpoint.com/"
}
Startup
services.Configure<ApplicationSettings>(Configuration.GetSection("ApplicationSettings"));
services.AddScoped<IGetItemsQuery, GetPhotosQuery>();
services.AddScoped<IHttpClientFactory, HttpClientFactory>();
I want to try something like below in my test
[Fact]
public void Test_Index()
{
// Arrange
var itemsQuery = new Mock<IGetItemsQuery>();
var controller = new HomeController(itemsQuery.Object);
// Act
var result = controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
Assert.Null(viewResult.ViewName);
}
This is creating mock IGetItemsQuery but this isn't mocking the actual IHttpClientFactory.
Is there a way to do this
Based on your design with the abstracted dependencies there would be no need to mock a client factory in order to unit test the controller.
As you have done in your test, you mock IGetItemsQuery, but you have not set it up to behave as expected when invoked in the test.
If, for example, the controller method under test look something like this
private readonly IGetItemsQuery getItemsQuery;
public HomeController(IGetItemsQuery getItemsQuery) {
this.getItemsQuery = getItemsQuery;
}
public async Task<IActionResult> Index() {
var url = "....";
var items = await getItemsQuery.Execute<MyItem>(url);
return View(items);
}
Then an isolated unit test for the Index action as the method under test could look something like
[Fact]
public async Task Index_Should_Return_View_With_Items() {
// Arrange
var itemsQuery = new Mock<IGetItemsQuery>();
var items = new MyItem[] {
new MyItem(),
new MyItem()
};
itemsQuery.Setup(_ => _.Execute<MyItem>(It.IsAny<string>()))
.ReturnsAsync(items);
var controller = new HomeController(itemsQuery.Object);
// Act
var result = await controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
Assert.Null(viewResult.ViewName);
}

Moq Unit Test case - ASP.NET MVC with WebAPI

I am trying to UnitTest my MVC Controller method, which internally makes call to an WebAPI(using HttpClient). I'm not able to figure out how can I fake the httpclient call, as it should not go for actual request. Below is my source code and unit test case. Test case fails, as the call goes for actual HttpRequest (An error occurred while sending the request. A connection with the server could not be established)
Base MVC Controller
public class BaseController : Controller
{
public virtual async Task<T> PostRequestAsync<T>(string endpoint, Object obj) where T : class
{
var address = "http://localhost:5001/api/Login";
StringContent json = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "");
using (var client = new HttpClient())
{
try
{
var response = await client.PostAsync(address, json); // Test case fails here
if (response.IsSuccessStatusCode)
{
string data = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(data);
}
return default(T);
}
catch (WebException)
{
throw;
}
}
}
}
Derived class Controller
public class AccountController : BaseController
{
public AccountController() : base()
{
}
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
if (ModelState.IsValid)
{
var result = await PostRequestAsync<ResultObject>(Constants.UserLoginAPI, model); // this is call for basecontroller method which actually has HttpClient call.
var output = JsonConvert.DeserializeObject<UserObject>(result.Object.ToString());
if (result.Succeeded && !string.IsNullOrEmpty(output.Email))
{
var userRoleInfo = await GetRequestAsync<List<UserRoleObject>>(string.Format(Constants.GetUserRoleInfoAPI, output.Email));
if (userRoleInfo != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, output.Email),
new Claim("Username", output.UserName)
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), new AuthenticationProperties { IsPersistent = model.RememberMe });
}
return View(new LoginViewModel());
}
}
return View(model);
}
}
TestMethod
[Fact]
public async Task LoginTest_Post_UserHasInValidCredentials()
{
// Arrange
var mockModel = new LoginViewModel { };
mockModel.Password = "TestPassword";
mockModel.Email = "test#test.com";
mockModel.RememberMe = false;
var commonResult = new CommonResult { Object = null, Succeeded = false, StatusCode = Common.Enums.ResponseStatusCodeEnum.Success };
var email = string.Empty;
var mockHttp = new MockHttpMessageHandler();
var mockBase = new Mock<BaseController>() { CallBase=true};
mockHttp.When("http://127.0.0.1:5001/*").Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON - using RichardSzalay.MockHttp;
//// Inject the handler or client into your application code
StringContent jsonInput = new StringContent(JsonConvert.SerializeObject(mockModel), Encoding.UTF8, "application/json");
var client = new HttpClient(mockHttp);
var response = await client.PostAsync("http://127.0.0.1:5001" + Constants.UserLoginAPI, jsonInput);
var json = await response.Content.ReadAsStringAsync();
mockBase.Setup(test => test.PostRequestAsync<CommonResult>(Constants.UserLoginAPI, mockModel)).Returns(Task.FromResult(CommonResult()));
var result = await accountController.Login(mockModel); // test case fails, as the call goes for actual HttpRequest (An error occurred while sending the request. A connection with the server could not be established)
//var viewResult = Assert.IsType<ViewResult>(result);
Assert.NotNull(commonResult);
Assert.False(commonResult.Succeeded);
Assert.Empty(email);
//Assert.NotNull(model.Email);
}
Tight coupling to HttpClient in the base controller makes it difficult to test derived classes in isolation. Review and refactor that code to follow DI.
No need to have a base controller and it is usually not advised.
Extract PostRequestAsync out into its own service abstraction and implementation.
public interface IWebService {
Task<T> PostRequestAsync<T>(string endpoint, Object obj) where T : class;
Task<T> GetRequestAsync<T>(string endpoint) where T : class;
}
public class WebService : IWebService {
static HttpClient client = new HttpClient();
public virtual async Task<T> PostRequestAsync<T>(string requestUri, Object obj) where T : class {
var content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "");
try {
var response = await client.PostAsync(requestUri, content); // Test case fails here
if (response.IsSuccessStatusCode) {
string data = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(data);
}
return default(T);
} catch (WebException) {
throw;
}
}
public async Task<T> GetRequestAsync<T>(string requestUri) where T : class {
try {
var response = await client.GetAsync(requestUri);
if (response.IsSuccessStatusCode) {
string data = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(data);
}
return default(T);
} catch (WebException) {
throw;
}
}
}
Refactor derived controllers to depend on the service abstraction
public class AccountController : Controller {
private readonly IWebService webService;
public AccountController(IWebService webService) {
this.webService = webService;
}
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) {
if (ModelState.IsValid) {
var result = await webService.PostRequestAsync<ResultObject>(Constants.UserLoginAPI, model);
if (result.Succeeded) {
var output = JsonConvert.DeserializeObject<UserObject>(result.Object.ToString());
if (output != null && !string.IsNullOrEmpty(output.Email)) {
var userRoleInfo = await webService.GetRequestAsync<List<UserRoleObject>>(string.Format(Constants.GetUserRoleInfoAPI, output.Email));
if (userRoleInfo != null) {
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, output.Email),
new Claim("Username", output.UserName)
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), new AuthenticationProperties { IsPersistent = model.RememberMe });
}
return View(new LoginViewModel());
}
}
}
return View(model);
}
}
This should now allow you to mock the dependency when testing in isolation without adverse side effects.
[Fact]
public async Task LoginTest_Post_UserHasInValidCredentials() {
// Arrange
var mockModel = new LoginViewModel { };
mockModel.Password = "TestPassword";
mockModel.Email = "test#test.com";
mockModel.RememberMe = false;
var commonResult = new CommonResult {
Object = null,
Succeeded = false,
StatusCode = Common.Enums.ResponseStatusCodeEnum.Success
};
var mockWebService = new Mock<IWebService>();
var accountController = new AccountController(mockWebService.Object) {
//HttpContext would also be needed
};
mockWebService
.Setup(test => test.PostRequestAsync<CommonResult>(Constants.UserLoginAPI, mockModel))
.ReturnsAsync(commonResult);
//
//Act
var result = await accountController.Login(mockModel);
//Assert
//...Make assertions here
}
I would inject an IHttpClient interface, and in release register a HttpClient wrapper that implements that interface.

Categories

Resources