Typed HTTP client and CustomWebApplicationFactory - c#

We are using .NET Core 3.1 to develop a REST API service. We would like to implement integration tests. We found this article which explains how to use WebApplicationFactory.
CustomWebApplicationFactory.cs
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((context, conf) =>
{
var p = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.test.json");
conf.AddJsonFile(p);
});
builder.ConfigureServices(services =>
{
var configuration = services.BuildServiceProvider().GetRequiredService<IConfiguration>();
services.AddSingleton<RestApiConfigurationClient>(configuration.GetSection("RestApi").Get<RestApiConfigurationClient>());
services.AddHttpClient<RestApiHttpClient>();
});
}
}
RestApiTestBase.cs
public class RestApiTestBase
{
private readonly RestApiHttpClient _restApiHttpClient;
protected RestApiTestBase(CustomWebApplicationFactory<Rest.Server.Startup> factory)
{
var scope = factory.Services.CreateScope();
_restApiHttpClient = scope.ServiceProvider.GetRequiredService<RestApiHttpClient>();
}
}
[CollectionDefinition("RestApi_test_collection")]
public class RestApiTestCollection : ICollectionFixture<CustomWebApplicationFactory<Rest.Server.Startup>>
{
}
RestApiHttpClient.cs
public class RestApiHttpClient : IProductsService
{
private readonly HttpClient _httpClient;
private readonly IProductsService _productsService;
public RestApiHttpClient(HttpClient httpClient, RestApiConfigurationClient configuration)
{
if (configuration == null)
{
throw new Exception("Configuration is not provided");
}
httpClient.BaseAddress = new Uri(configuration.URL);
httpClient.SetAuthorizationHeader(configuration.Username, configuration.Password);
httpClient.Timeout = TimeSpan.FromMilliseconds(configuration.TimeoutMs);
_httpClient = httpClient;
_productsService = new ProductsService(this);
}
public HttpClient GetHttpClient()
{
return _httpClient;
}
public async Task<GetProductByIdResponse> GetProductById(int id)
{
return await _productsService.GetProductById(id);
}
}
ProductsService.cs
public class ProductsService : IProductsService
{
private readonly HttpClient _httpClient;
public ProductsService(RestApiHttpClient httpClient)
{
_httpClient = httpClient.GetHttpClient();
}
public async Task<GetProductByIdResponse> GetProductById(int id)
{
var response = await _httpClient.GetAsync($"Products/{id}");
return JsonConvert.DeserializeObject<GetProductByIdResponse>(await response.Content.ReadAsStringAsync());
}
}
How can we inject HttpClient which can be created by WebApplicationFactory<TEntryPoint>.CreateClient() into typed HTTP client RestApiHttpClient?

Related

How to return the same request scoped value from a class in a thread safe way?

I need to create a wrapper class for IAzureMediaServicesClient which if injected as a scoped service (in a single http request) can return same client object to the callers.
This is the current wrapper code that needs to be fixed.
public class AzureMediaServicesClientProvider : IAzureMediaServicesClientProvider
{
private readonly IConfiguration _configuration;
public AzureMediaServicesClientProvider(IConfiguration configuration)
{
_configuration = configuration;
}
public async Task<IAzureMediaServicesClient> GetClient()
{
ServiceClientCredentials credentials = await ApplicationTokenProvider.LoginSilentAsync(
_configuration[ConfigurationConstants.AadTenantId],
_configuration[ConfigurationConstants.AmsAadClientId],
_configuration[ConfigurationConstants.AmsAadSecret]);
return new AzureMediaServicesClient(new Uri(_configuration[ConfigurationConstants.ArmEndpoint]), credentials)
{
SubscriptionId = _configuration[ConfigurationConstants.SubscriptionId],
};
}
}
The class is registered as a Scoped service in DI
public static IServiceCollection AddAzureMediaServiceClient(this IServiceCollection serviceCollection)
{
return serviceCollection.AddScoped<IAzureMediaServicesClientProvider, AzureMediaServicesClientProvider>();
}
and a sample usage in code
public async Task<Job> CreateJobAsync(string transformName, Job job)
{
IAzureMediaServicesClient client = await _azureMediaServicesClientFactory.GetClient();
return await client.Jobs.CreateAsync(_resourceGroupName, _accountName, transformName, job.Name, job);
}
public async Task<Job> GetJobAsync(string transformName, string jobName)
{
IAzureMediaServicesClient client = await _azureMediaServicesClientFactory.GetClient();
return await client.Jobs.GetAsync(_resourceGroupName, _accountName, transformName, jobName);
}
Now the methods GetJobAsync and CreateJobAsync can be used in the same request and currently in such scenario for each of them a new client would be created. How can the provider class be rewritten so that in a single request same client object would be returned ? (I know I could inject it in a higher level and just pass the value to these methods but this is a simplified example and the real world use case would require a lot of refactoring to achieve this).
public async Task TestMethod()
{
var job = await GetJobAsync(...);
// Do some code modifications
await CreateJobAsync(...);
// How can we make sure here that both GetJobAsync and
// CreateJobAsync used the same client AzureMediaServicesClient instance ?
}
Below sample shows the intent but wouldn't be thread safe if I understand correctly ?
public class AzureMediaServicesClientProvider : IAzureMediaServicesClientProvider
{
private readonly IConfiguration _configuration;
private IAzureMediaServicesClient _client;
public AzureMediaServicesClientProvider(IConfiguration configuration)
{
_configuration = configuration;
}
public async Task<IAzureMediaServicesClient> GetClient()
{
if (_client == null)
{
ServiceClientCredentials credentials = await ApplicationTokenProvider.LoginSilentAsync(
_configuration[ConfigurationConstants.AadTenantId],
_configuration[ConfigurationConstants.AmsAadClientId],
_configuration[ConfigurationConstants.AmsAadSecret]);
_client = new AzureMediaServicesClient(new Uri(_configuration[ConfigurationConstants.ArmEndpoint]), credentials)
{
SubscriptionId = _configuration[ConfigurationConstants.SubscriptionId],
};
}
return _client;
}
}
You can use the AsyncLazy<T> from the package Microsoft.VisualStudio.Threading:
public class AzureMediaServicesClientProvider : IAzureMediaServicesClientProvider
{
private readonly IConfiguration _configuration;
private readonly AsyncLazy<IAzureMediaServicesClient> _lazyClient;
public AzureMediaServicesClientProvider(IConfiguration configuration)
{
_configuration = configuration;
_lazyClient = new AsyncLazy<IAzureMediaServicesClient>(CreateClient);
}
public Task<IAzureMediaServicesClient> GetClient()
{
return _lazyClient.GetValueAsync();
}
private async Task<IAzureMediaServicesClient> CreateClient()
{
ServiceClientCredentials credentials = await ApplicationTokenProvider.LoginSilentAsync(
_configuration[ConfigurationConstants.AadTenantId],
_configuration[ConfigurationConstants.AmsAadClientId],
_configuration[ConfigurationConstants.AmsAadSecret]);
return new AzureMediaServicesClient(new Uri(_configuration[ConfigurationConstants.ArmEndpoint]), credentials)
{
SubscriptionId = _configuration[ConfigurationConstants.SubscriptionId],
};
}
}
AsyncLazy<T> is thread-safe for all members.

IClassFixture called multipletimes with locked Simple Injector Container

i'm trying to test my controllers with IClassFixture<WebApplicationFactory<Startup>>, but when i run multiples tests i get the error:
System.InvalidOperationException : The container can't be changed
after the first call to GetInstance, GetAllInstances, Verify, and some
calls of GetRegistration. Please see https://simpleinjector.org/locked
to understand why the container is locked. The following stack trace
describes the location where the container was locked:
public class TestControllerTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly HttpClient _httpClient;
private readonly TestDbContext _dbContext;
private readonly AuthenticationClientBuilder<MicrosoftPatternAdministratorAuthHandler, Startup> _builder;
private readonly WebApplicationFactory<Startup> _factory;
public LicenseControllerTests(WebApplicationFactory<Startup> factory)
{
_factory = factory;
_builder = new AuthenticationClientBuilder<MicrosoftPatternAdministratorAuthHandler, Startup>();
_dbContext = new TestDbContext(new DbContextOptions<TestDbContext>());
_dbContext.Database.SetConnectionString(_builder.GetConnectionString());
DbInitializer.Initialize(_dbContext);
_httpClient = _builder.BuildAuthenticatedClient(_factory);
}
}
In the callstack, i see that the error ocourred in the line: _httpClient = _builder.BuildAuthenticatedClient(_factory);
The code of this class is:
namespace Namespace_X
{
public class AuthenticationClientBuilder<TAuthenticationHandler, TStartup> : IDisposable
where TAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
where TStartup : class
{
private WebApplicationFactory<TStartup> _factory;
private readonly string _connectionString;
public AuthenticationClientBuilder()
{
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
_connectionString = config["AppSettings:ConnectionString"];
}
public HttpClient BuildAuthenticatedClient(WebApplicationFactory<TStartup> factory)
{
_factory = factory;
return _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("TestAuthentication")
.AddScheme<AuthenticationSchemeOptions, TAuthenticationHandler>("TestAuthentication", null);
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(TestDbContext));
if (descriptor != null)
{
services.Remove(descriptor);
services.AddDbContext<TestDbContext>((options, context) =>
{
context.UseSqlServer(_connectionString);
});
}
});
}).CreateClient();
}
public string GetConnectionString()
{
return _connectionString;
}
public void Dispose()
{
_factory.Dispose();
}
}
}
In the startup the exception is throw when the container try to register the DbContext:
container.Register(() =>
{
var options = new DbContextOptionsBuilder<TestDbContext>().UseSqlServer().Options;
return new TestDbContext(options);
}, Lifestyle.Transient);
When i run one test per time they work.
Any hint? Thx in advance

DI issue in .NET Core

I have an odata query builder class that I am using to build my odata string that is desterilising the result based on the object that called it.
public class UosOdataQueryBuilder<T>
{
private readonly Dictionary<string, string> _queryOptions;
private readonly IHttpClientFactory _clientFactory;
private readonly ILogger _logger;
public UosOdataQueryBuilder([FromServices] IHttpClientFactory clientFactory, [FromServices] ILogger logger)
{
_queryOptions = new Dictionary<string, string>();
_clientFactory = clientFactory;
_logger = logger;
}
public UosOdataQueryBuilder<T> WithFilter(string filter)
{
_queryOptions.Add("$filter", filter);
return this;
}
public UosOdataQueryBuilder<T> Skip(int skip)
{
_queryOptions.Add("$skip", skip.ToString());
return this;
}
public UosOdataQueryBuilder<T> Top(int top)
{
_queryOptions.Add("$top", top.ToString());
return this;
}
public UosOdataQueryBuilder<T> WithNoInlineCount()
{
_queryOptions.Add("$inlinecount", "none");
return this;
}
public UosOdataQueryBuilder<T> OrderBy(string orderBy)
{
_queryOptions.Add("$orderby", orderBy);
return this;
}
public async Task<UosOdataReponse<T>> ExecuteQueryAsync(string elementName = "")
{
var result = new UosOdataReponse<T>();
try
{
var authToken = AppSettings.PlatformBearerToken;
var queryParameters = new List<string>();
foreach (var option in _queryOptions)
queryParameters.Add($"{option.Key}={option.Value}");
var queryParametersCombined = string.Join("&", queryParameters);
var oDataElementName = (elementName == "") ? typeof(T).Name : elementName;
var baseUrl = AppSettings.PlatformBaseUri;
var client = _clientFactory.CreateClient("UOS");
var request = new HttpRequestMessage(
HttpMethod.Get,
new Uri(baseUrl + $"/uos/v4/odata/{oDataElementName}" + queryParametersCombined));
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var data = await response.Content.ReadAsStringAsync();
result = JsonConvert.DeserializeObject<UosOdataReponse<T>>(data);
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
return result;
}
}
I have setup the client in startup
services.AddHttpClient("UOS", c =>
{
c.BaseAddress = new Uri(Configuration.GetValue<string>("PlatformBaseUri") + "uos/v4/");
c.DefaultRequestHeaders.Add("Accept", "application/json");
c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Configuration.GetValue<string>("PlatformBearerToken"));
//c.Timeout = new TimeSpan(0, 0, 30);
});
When I create a new instance of this from another method it is requiring that I pass in the clientFactory and logger.
protected async Task<int> GetUosOdataCount(string filter)
{
var result = new List<T>();
try
{
var countCheck = await new UosOdataQueryBuilder<T>()
.WithFilter(filter)
.Top(1)
.ExecuteQueryAsync();
return countCheck.Count;
}
catch (Exception ex)
{
//CustomLogger.LogError(GetType().FullName, "GetUosOdata", ex.Message);
}
}
In .NET Framework I would remove the parameters from the constructor of the UosOdataQueryBuilder and resolve the dependencies within it. For Example:
_uosUserAttributeRepository = GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(IUosUserAttributeRepository)) as IUosUserAttributeRepository;
But I am not sure how to achieve in .NET Core. Any suggestions?
You can create an interface for UosOdataQueryBuilder<T> and register it into DI generically. some thing like this:
public interface IUosOdataQueryBuilder<T>
{
Task<T> SomeMethod();
}
public class UosOdataQueryBuilder<T> : IUosOdataQueryBuilder<T>
{
private readonly Dictionary<string, string> _queryOptions;
private readonly IHttpClientFactory _clientFactory;
private readonly ILogger<UosOdataQueryBuilder<T>> _logger;
public UosOdataQueryBuilder(IHttpClientFactory clientFactory, ILogger<UosOdataQueryBuilder<T>> logger)
{
_queryOptions = new Dictionary<string, string>();
_clientFactory = clientFactory;
_logger = logger;
}
public Task<T> SomeMethod()
{
return default;
}
}
And in ConfigureServices in startup write this:
services.AddScoped(typeof(IUosOdataQueryBuilder<>), typeof(UosOdataQueryBuilder<>));
And in your controller inject the IUosOdataQueryBuilder:
private readonly IUosOdataQueryBuilder<YourClass> _uosOdataQueryBuilder;
public YourController( IUosOdataQueryBuilder<YourClass> uosOdataQueryBuilder)
{
_uosOdataQueryBuilder = uosOdataQueryBuilder;
}
you can register IUosOdataQueryBuilder as Singleton but for prevent memory leak you should inject IServiceScopeFactory in concrete class to get registered service in your methods not in constructor.
I'd agree with comment to stick with injection but you can retrieve the dependencies within the class using IServiceProvider
e.g
MyMethod(IServiceProvider serviceProvider)
{
MyService svc = (MyService)serviceProvider.GetService(typeof(MyService));
...

Property injection

I'm trying make a telegram bot with reminder. I'm using Telegram.Bot 14.10.0, Quartz 3.0.7, .net core 2.0. The first version should : get message "reminder" from telegram, create job (using Quartz) and send meaasage back in 5 seconds.
My console app with DI looks like:
Program.cs
static IBot _botClient;
public static void Main(string[] args)
{
// it doesn't matter
var servicesProvider = BuildDi(connecionString, section);
_botClient = servicesProvider.GetRequiredService<IBot>();
_botClient.Start(appModel.BotConfiguration.BotToken, httpProxy);
var reminderJob = servicesProvider.GetRequiredService<IReminderJob>();
reminderJob.Bot = _botClient;
Console.ReadLine();
_botClient.Stop();
// it doesn't matter
}
private static ServiceProvider BuildDi(string connectionString, IConfigurationSection section)
{
var rJob = new ReminderJob();
var sCollection = new ServiceCollection()
.AddSingleton<IBot, Bot>()
.AddSingleton<ReminderJob>(rJob)
.AddSingleton<ISchedulerBot>(s =>
{
var schedBor = new SchedulerBot();
schedBor.StartScheduler();
return schedBor;
});
return sCollection.BuildServiceProvider();
}
Bot.cs
public class Bot : IBot
{
static TelegramBotClient _botClient;
public void Start(string botToken, WebProxy httpProxy)
{
_botClient = new TelegramBotClient(botToken, httpProxy);
_botClient.OnReceiveError += BotOnReceiveError;
_botClient.OnMessage += Bot_OnMessage;
_botClient.StartReceiving();
}
private static async void Bot_OnMessage(object sender, MessageEventArgs e)
{
var me = wait _botClient.GetMeAsync();
if (e.Message.Text == "reminder")
{
var map= new Dictionary<string, object> { { ReminderJobConst.ChatId, e.Message.Chat.Id.ToString() }, { ReminderJobConst.HomeWordId, 1} };
var job = JobBuilder.Create<ReminderJob>().WithIdentity($"{prefix}{rnd.Next()}").UsingJobData(new JobDataMap(map)).Build();
var trigger = TriggerBuilder.Create().WithIdentity($"{prefix}{rnd.Next()}").StartAt(DateTime.Now.AddSeconds(5).ToUniversalTime())
.Build();
await bot.Scheduler.ScheduleJob(job, trigger);
}
}
}
Quartz.net not allow use constructor with DI. That's why I'm trying to create property with DI.
ReminderJob.cs
public class ReminderJob : IJob
{
static IBot _bot;
public IBot Bot { get; set; }
public async Task Execute(IJobExecutionContext context)
{
var parameters = context.JobDetail.JobDataMap;
var userId = parameters.GetLongValue(ReminderJobConst.ChatId);
var homeWorkId = parameters.GetLongValue(ReminderJobConst.HomeWordId);
await System.Console.Out.WriteLineAsync("HelloJob is executing.");
}
}
How can I pass _botClient to reminderJob in Program.cs?
If somebody looks for answer, I have one:
Program.cs (in Main)
var schedBor = servicesProvider.GetRequiredService<ISchedulerBot>();
var logger = servicesProvider.GetRequiredService<ILogger<DIJobFactory>>();
schedBor.StartScheduler();
schedBor.Scheduler.JobFactory = new DIJobFactory(logger, servicesProvider);
DIJobFactory.cs
public class DIJobFactory : IJobFactory
{
static ILogger<DIJobFactory> _logger;
static IServiceProvider _serviceProvider;
public DIJobFactory(ILogger<DIJobFactory> logger, IServiceProvider sp)
{
_logger = logger;
_serviceProvider = sp;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
IJobDetail jobDetail = bundle.JobDetail;
Type jobType = jobDetail.JobType;
try
{
_logger.LogDebug($"Producing instance of Job '{jobDetail.Key}', class={jobType.FullName}");
if (jobType == null)
{
throw new ArgumentNullException(nameof(jobType), "Cannot instantiate null");
}
return (IJob)_serviceProvider.GetRequiredService(jobType);
}
catch (Exception e)
{
SchedulerException se = new SchedulerException($"Problem instantiating class '{jobDetail.JobType.FullName}'", e);
throw se;
}
}
// get from https://github.com/quartznet/quartznet/blob/139aafa23728892b0a5ebf845ce28c3bfdb0bfe8/src/Quartz/Simpl/SimpleJobFactory.cs
public void ReturnJob(IJob job)
{
var disposable = job as IDisposable;
disposable?.Dispose();
}
}
ReminderJob.cs
public interface IReminderJob : IJob
{
}
public class ReminderJob : IReminderJob
{
ILogger<ReminderJob> _logger;
IBot _bot;
public ReminderJob(ILogger<ReminderJob> logger, IBot bot)
{
_logger = logger;
_bot = bot;
}
public async Task Execute(IJobExecutionContext context)
{
var parameters = context.JobDetail.JobDataMap;
var userId = parameters.GetLongValue(ReminderJobConst.ChatId);
var homeWorkId = parameters.GetLongValue(ReminderJobConst.HomeWordId);
await _bot.Send(userId.ToString(), "test");
}
}

Get remote ip while using Owin.Testing

I'm using Owin.Testing as test env. In my controller i need to get remote ip address from the caller.
//in my controller method
var ip = GetIp(Request);
Util
private string GetIp(HttpRequestMessage request)
{
return request.Properties.ContainsKey("MS_HttpContext")
? (request.Properties["MS_HttpContext"] as HttpContextWrapper)?.Request?.UserHostAddress
: request.GetOwinContext()?.Request?.RemoteIpAddress;
}
As a result Properties does not contains MS_HttpContext and RemoteIpAddress of OwinContext is null.
Is there any option to get IP?
Found the solution. Use testing middleware for this. Everything in your tests project:
public class IpMiddleware : OwinMiddleware
{
private readonly IpOptions _options;
public IpMiddleware(OwinMiddleware next, IpOptions options) : base(next)
{
this._options = options;
this.Next = next;
}
public override async Task Invoke(IOwinContext context)
{
context.Request.RemoteIpAddress = _options.RemoteIp;
await this.Next.Invoke(context);
}
}
Handler:
public sealed class IpOptions
{
public string RemoteIp { get; set; }
}
public static class IpMiddlewareHandler
{
public static IAppBuilder UseIpMiddleware(this IAppBuilder app, IpOptions options)
{
app.Use<IpMiddleware>(options);
return app;
}
}
Testing startup:
public class TestStartup : Startup
{
public new void Configuration(IAppBuilder app)
{
app.UseIpMiddleware(new IpOptions {RemoteIp = "127.0.0.1"});
base.Configuration(app);
}
}
And then create test server via TestStartup:
TestServer = TestServer.Create<TestStartup>();

Categories

Resources