- Blog
#Azure #Technology
Securing Azure Virtual Machines: Using Azure Bastion and Just-In-Time (JIT) Access
- 18/11/2024
Reading time 5 minutes
Every now and then some changes are needed for Dependency Injection configuration (new classes are added, scope is changed, configuration is changed and so on). I’m using .Net core, so DI is configured in Startup.cs file.
The goal is to build integration test that validates dependency injection configuration is correct. Test should be executed before deploying the application.
This article is continuation of https://zure.com/blog/validate-authentication-is-enabled-by-using-integration-test/
Source code can be found in https://github.com/sovaska/automaticintegrationtests
First step is to build REST endpoint for testing dependency injection configuration. Endpoint is called /api/metadata/dependencyinjection. Sample application does not have authentication, but if the following code will be used in real application, authentication needs to be added.
using Microsoft.AspNetCore.Mvc;
using Countries.Web.Contracts;
using System.Collections.Generic;
using Countries.Web.Models;
namespace Countries.Web.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class MetadataController : ControllerBase
{
private readonly IMetadataService _metadataService;
public MetadataController(IMetadataService metadataService)
{
_metadataService = metadataService;
}
[HttpGet("dependencyinjection")]
public ActionResult<DIMetadata> DependencyInjection()
{
var results = _metadataService.GetDependencyInjectionProblems(HttpContext.RequestServices);
return Ok(results);
}
}
}
The implementation of GetDependencyInjectionProblems() can be found in MetadataService class. Function uses IServiceProvider to inject all other services in IServiceCollection than the ones having ContainsGenericParameters set to true. If injection succeeds service name is added to ValidTypes list, and if it fails it is added to Problems list with failure reason.
using System.Linq;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Countries.Web.Contracts;
using Countries.Web.Models;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace Countries.Web.Services
{
public class MetadataService : IMetadataService
{
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
private readonly IServiceCollection _serviceCollection;
public MetadataService(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
IServiceCollection serviceCollection)
{
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
_serviceCollection = serviceCollection;
}
public DIMetadata GetDependencyInjectionProblems(IServiceProvider serviceProvider)
{
var result = new DIMetadata();
foreach (var service in _serviceCollection)
{
var serviceType = service.ServiceType as System.Type;
try
{
if (serviceType.ContainsGenericParameters)
{
result.NotValidatedTypes.Add(new ServiceMetadata { ServiceType = serviceType.ToString(), Reason = "Type ContainsGenericParameters == true" });
continue;
}
var x = serviceProvider.GetService(service.ServiceType);
result.ValidTypes.Add(serviceType.ToString());
}
catch (Exception e)
{
result.Problems.Add(new ServiceMetadata { ServiceType = serviceType.ToString(), Reason = e.Message });
}
}
return result;
}
}
}
Please remember to register MetadataService in Startup class ConfigureServices -method:
services.AddSingleton<IMetadataService, MetadataService>();
You also need to register IServiceColletion as Singleton instance in Startup class ConfigureServices -method:
services.AddSingleton(services);
I created one DI problem on purpose (I injected ICountriesService in CountriesInMemoryRepository), and here are result /api/metadata/dependencyinjection endpoint returns for sample application (most of validTypes are removed to keep sample shorter):
{
"problems": [
{
"serviceType": "Countries.Web.Contracts.ICountriesInMemoryRepository",
"reason": "A circular dependency was detected for the service of type 'Countries.Web.Contracts.ICountriesInMemoryRepository'.rnCountries.Web.Contracts.ICountriesInMemoryRepository(Countries.Web.Repositories.CountriesInMemoryRepository) -> Countries.Web.Contracts.ICountriesService(Countries.Web.Services.CountriesService) -> Countries.Web.Contracts.ICountriesInMemoryRepository"
},
{
"serviceType": "Countries.Web.Contracts.ICountriesService",
"reason": "A circular dependency was detected for the service of type 'Countries.Web.Contracts.ICountriesService'.rnCountries.Web.Contracts.ICountriesService(Countries.Web.Services.CountriesService) -> Countries.Web.Contracts.ICountriesInMemoryRepository(Countries.Web.Repositories.CountriesInMemoryRepository) -> Countries.Web.Contracts.ICountriesService"
}
],
"notValidatedTypes": [
{
"serviceType": "Microsoft.Extensions.Options.IOptions`1[TOptions]",
"reason": "Type ContainsGenericParameters == true"
},
{
"serviceType": "Microsoft.Extensions.Options.IOptionsSnapshot`1[TOptions]",
"reason": "Type ContainsGenericParameters == true"
},
{
"serviceType": "Microsoft.Extensions.Options.IOptionsMonitor`1[TOptions]",
"reason": "Type ContainsGenericParameters == true"
},
{
"serviceType": "Microsoft.Extensions.Options.IOptionsFactory`1[TOptions]",
"reason": "Type ContainsGenericParameters == true"
},
{
"serviceType": "Microsoft.Extensions.Options.IOptionsMonitorCache`1[TOptions]",
"reason": "Type ContainsGenericParameters == true"
},
{
"serviceType": "Microsoft.Extensions.Logging.ILogger`1[TCategoryName]",
"reason": "Type ContainsGenericParameters == true"
},
{
"serviceType": "Microsoft.Extensions.Logging.Configuration.ILoggerProviderConfiguration`1[T]",
"reason": "Type ContainsGenericParameters == true"
},
{
"serviceType": "Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper`1[TModel]",
"reason": "Type ContainsGenericParameters == true"
},
{
"serviceType": "Microsoft.Extensions.Http.ITypedHttpClientFactory`1[TClient]",
"reason": "Type ContainsGenericParameters == true"
}
],
"validTypes": [
"Microsoft.AspNetCore.Mvc.Cors.CorsAuthorizationFilter",
"Microsoft.Extensions.Options.IConfigureOptions`1[Microsoft.AspNetCore.Mvc.Infrastructure.MvcCompatibilityOptions]",
"Microsoft.Extensions.Http.HttpMessageHandlerBuilder",
"System.Net.Http.IHttpClientFactory",
"Microsoft.Extensions.Http.IHttpMessageHandlerBuilderFilter",
"Countries.Web.Contracts.IRestCountriesService",
"Microsoft.Extensions.Options.IConfigureOptions`1[Microsoft.Extensions.Http.HttpClientFactoryOptions]",
"Microsoft.Extensions.Options.IConfigureOptions`1[Microsoft.Extensions.Http.HttpClientFactoryOptions]",
"Countries.Web.Contracts.IMetadataService",
"Microsoft.Extensions.DependencyInjection.IServiceCollection"
]
}
This metadata will be used in integration test, and if there are any items in Problems -list, test will fail.
I’m using XUnit as test framework.
It’s important that test specific startup class used in original article will not be used in this test. It is mandatory to test DI as it will be in application hosted in production.
I extended original base class for tests with ReadDependencyInjectionMetadataAsync() function. Function will call /api/metadata/dependencyinjection endpoint:
protected async Task<DIMetadata> ReadDependencyInjectionMetadataAsync()
{
using (var msg = new HttpRequestMessage(HttpMethod.Get, BuildUri("Metadata", parameters: "/dependencyinjection").ToString()))
{
using (var response = await Client.SendAsync(msg).ConfigureAwait(false))
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
if (!response.IsSuccessStatusCode)
{
return null;
}
ValidateHeaders(response.Headers);
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (string.IsNullOrEmpty(content))
{
return null;
}
return JsonConvert.DeserializeObject<DIMetadata>(content);
}
}
}
The test class doesn’t do much, it just
Here is the implementation of test class:
using Xunit;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Testing;
namespace Countries.Tests.ControllerTests
{
public class DependencyInjectionTests : ControllerTestBase, IClassFixture<WebApplicationFactory<Web.Startup>>
{
public DependencyInjectionTests(WebApplicationFactory<Web.Startup> factory)
: base(factory.CreateClient())
{
}
[Fact]
public async Task Test()
{
var diMetadata = await ReadDependencyInjectionMetadataAsync().ConfigureAwait(false);
Assert.NotNull(diMetadata);
Assert.True(!diMetadata.Problems.Any());
}
}
}
When test is executed, it succeeds since dependency injection configuration in sample application is correct.
Our newsletters contain stuff our crew is interested in: the articles we read, Azure news, Zure job opportunities, and so forth.
Please let us know what kind of content you are most interested about. Thank you!