ConfigurationProvider with other dependencies - c#

I've implemented my customs IConfigurationProvider and IConfigurationSource.
public class MyConfigurationSource : IConfigurationSource
{
public string Foo { get; set; }
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new MyConfigurationProvider(this);
}
}
internal class MyConfigurationProvider : ConfigurationProvider
{
public MyConfigurationSource Source { get; };
public MyConfigurationProvider()
{
Source = source
}
public override void Load()
{
// I'd like to assign here my configuration data by using some dependencies
Data = ....
}
}
I do the build of my Configuration in the Startup constructor (I override the configuration created by CreateDefaultBuilder):
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables()
.AddMyConfiguration("myfoovalue")
.Build();
Extension method:
public static IConfigurationBuilder AddMyConfiguration(this IConfigurationBuilder builder, string foo)
{
return builder.Add(new MyConfigurationSource
{
Foo = url
});
}
I wish I could somehow inject services to be used in Load method. The problem here is that the configuration build is done in the Startup constructor. I can only inject dependencies that I have available in this constructor: IWebHostEnvironment, IHostEnvironment, IConfiguration and all I added when I built the WebHost. Also these dependencies would have to be passed the moment I call the AddMyConfiguration extension method. How could I use dependencies that don't even exist at that moment?

A bit late answer.
It's obvious there's no way to use the container, that was built using Startup.ConfigureServices, in the MyConfigurationSource/MyConfigurationProvider simply because by the time ConfigurationBuilder.Build is invoked, ServiceCollection.BuildServiceProvider has not been invoked.
A typical workaround would be to create another instance of IServiceProvider with required configuration and use it inside MyConfiguration....
Something like
internal class MyConfigurationProvider : ConfigurationProvider
{
public MyConfigurationSource Source { get; };
public MyConfigurationProvider()
{
Source = source;
ServiceProvider = BuildServiceProvider();
}
public override void Load()
{
// I'd like to assign here my configuration data by using some dependencies
Data = ServiceProvider.GetRequiredService<IMyService>().GetData();
}
protected virtual void ConfigureServices(IServiceCollection services)
{
services.AddMyService();
// etc.
}
private IServiceProvider BuildServiceProvider()
{
var services = new ServiceCollection();
ConfigureServices(services);
return services.BuildServiceProvider();
}
}
but this might not always be appropriate so I would also consider setting the value directly (though I didn't find any official information about how good this approach is)
public class Startup
{
public void Configure(IApplicationBuilder app)
{
...
app.SetupMyConfiguration();
...
}
}
...
public static class ApplicationBuilderExtensions
{
public static IApplicationBuilder SetupMyConfiguration(this IApplicationBuilder app)
{
var configuration = app
.ApplicationServices
.GetRequiredService<IConfiguration>(); // alternatively IOptions<MyOptions>
var myService = app
.ApplicationServices
.GetRequiredService<IMyService>();
configuration["MyKey"] = myService.GetData("MyKey");
}
}
UPD.
There's also an alternative with using strongly typed options object and IConfigureOptions.
public class MyConfigurationBuilder : IConfigureOptions<MyConfiguration>
{
private readonly IConfiguration _configuration;
private readonly IMyService _service;
public MyConfigurationBuilder(
IConfiguration configuration,
IMyService service)
{
_configuration = configuration;
_service = service;
}
public void Configure(MyConfiguration myConfiguration)
{
// you may set static configuration values
_configuration
.GetSection(nameof(MyConfiguration))
.Bind(myConfiguration);
// or from DI
myConfiguration.Data = _service.GetData();
// here we still can update IConfiguration,
// though it doesn't seem to be a good idea
_configuration["MyKey"] = _service.GetData("MyKey");
}
}
services.AddSingleton<IConfigureOptions<MyConfiguration>, MyConfigurationBuilder>();
or inject dependencies directly into MyConfiguration
services.Configure<MyConfiguration>(
serviceProvider =>
ActivatorUtilities.CreateInstance<MyConfiguration>(serviceProvider, "staticConfigValue"));
public class MyConfiguration
{
public MyConfiguration(string staticValue, IMyService service)
{
...
}
}
public class Service
{
public Service(IOptions<MyConfiguration> options) {}
}

Related

.NET Core WPF DI'd Named OptionsMonitor always null

I have a .NET Core 3.1 WPF application, and I'm attempting to inject configuration values into a service in a lower layer (Infrastructure) through an IOptionsMonitor<T> using Named Options. Values are always populated as null.
Here's the file contents:
appsettings.json
{
"Repositories": {
"Repo1": {
"BaseUrl": "default1",
"Fragment1": "defaultFragment1"
},
"Repo2": {
"BaseUrl": "default2",
"Fragment1": "defaultFragment2"
}
}
}
appsettings.development.json
{
"Repositories": {
"Repo1": {
"BaseUrl": "https://developmentRepo1.com",
"Fragment1": "developmentFragment1"
},
"Repo2": {
"BaseUrl": "https://developmentRepo2.com",
"Fragment1": "developmentFragment2"
}
}
}
I have an Environment Variable which is DOTNET_ENVIRONMENT and it's set to development.
On the main App.xaml.cs I have this:
App.xaml.cs:
protected override void OnStartup(StartupEventArgs e)
{
var builder = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile("appsettings.development.json", optional: true, reloadOnChange: true);
Configuration = builder.Build();
var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection);
ServiceProvider = serviceCollection.BuildServiceProvider();
var mainWindow = ServiceProvider.GetRequiredService<PricingWizard>();
mainWindow.Show();
}
private void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.Configure<RepositoryConfiguration>(
RepositoryConfiguration.Repo1Config,
Configuration.GetSection("Repositories:Repo1"));
services.AddScoped<Infrastructure.SomeContext>();
}
My configuration Class looks something like this:
RepositoryConfiguration.cs:
public class RepositoryConfiguration
{
public const string Repo1Config = "Repo1Config";
public string BaseUrl { get; set; }
public string Fragment1 { get; set; }
}
And the service in the layer below:
SomeContext.cs:
public class SomeContext : IDisposable
{
private readonly RepositoryConfiguration configuration;
public SomeContext(IOptionsMonitor<RepositoryConfiguration> configuration)
{
this.configuration = configuration.Get(RepositoryConfiguration.Repo1Config);
}
public void SomeMethod()
{
var url = configuration.BaseUrl;
}
}
For some reason, both BaseUrl and Fragment1 are always null.
Please check the appsettings files, make sure you are using the correct file and it contains the related value, and check the code where you call the SomeMethod() method.
According to your code, I have tested your code on my application, it seems that everything works well. Code as below:
Code in the SomeContext.cs:
public class SomeContext : IDisposable
{
private readonly RepositoryConfiguration configuration;
public SomeContext(IOptionsMonitor<RepositoryConfiguration> configuration)
{
this.configuration = configuration.Get(RepositoryConfiguration.Repo1Config);
}
public void Dispose()
{
throw new NotImplementedException();
}
public string SomeMethod()
{
var url = configuration.BaseUrl;
return url;
}
}
Code in the App.xaml.cs:
public partial class App : Application
{
public IServiceProvider ServiceProvider { get; private set; }
public IConfiguration Configuration { get; private set; }
protected override void OnStartup(StartupEventArgs e)
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
Configuration = builder.Build();
// Create a service collection and configure our dependencies
var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection);
// Build the our IServiceProvider and set our static reference to it
ServiceProvider = serviceCollection.BuildServiceProvider();
var mainWindow = ServiceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
private void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.Configure<AppSettings>(Configuration.GetSection(nameof(AppSettings)));
services.AddScoped<ISampleService, SampleService>();
services.Configure<RepositoryConfiguration>(RepositoryConfiguration.Repo1Config, Configuration.GetSection("Repositories:Repo1"));
services.AddScoped<SomeContext>();
services.AddTransient(typeof(MainWindow));
}
}
Code in the MainWindow.xaml.cs
public partial class MainWindow : Window
{
private readonly ISampleService sampleService;
private readonly AppSettings settings;
private readonly SomeContext someContext;
public MainWindow(ISampleService sampleService, IOptions<AppSettings> settings, SomeContext context)
{
InitializeComponent();
this.sampleService = sampleService;
this.settings = settings.Value;
this.someContext = context;
lblBaseUrl.Content = context.SomeMethod();
}
private void ButtonExit_Click(object sender, RoutedEventArgs e)
{
Application.Current.Shutdown();
}
}
The appsettings.json content as below:
{
"AppSettings": {
"StringSetting": "Value",
"IntegerSetting": 42,
"BooleanSetting": true
},
"Repositories": {
"Repo1": {
"BaseUrl": "https://developmentRepo1.com",
"Fragment1": "developmentFragment1"
},
"Repo2": {
"BaseUrl": "https://developmentRepo2.com",
"Fragment1": "developmentFragment2"
}
}
}
It seems that the code works well on my side, screenshot as below:
Here is an article about Using .NET Core 3.0 Dependency Injection and Service Provider with WPF, you could check it.

How to setup the DI container in a .NET Core console app?

I created a new .NET Core console app and installed the following packages
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.EnvironmentVariables
Microsoft.Extensions.Configuration.Json
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Options
Microsoft.Extensions.Options.ConfigurationExtensions
I created a appsettings.json file for the configuration
{
"app": {
"foo": "bar"
}
}
and I want to map those values to a class
internal class AppOptions
{
public string Foo { get; set; }
}
I also want to validate the options during configuration so I added a validating class
internal class AppOptionsValidator : IValidateOptions<AppOptions>
{
public ValidateOptionsResult Validate(string name, AppOptions options)
{
IList<string> validationFailures = new List<string>();
if (string.IsNullOrEmpty(options.Foo))
validationFailures.Add("Foo is required.");
return validationFailures.Any()
? ValidateOptionsResult.Fail(validationFailures)
: ValidateOptionsResult.Success;
}
}
I want to setup the DI container and created a testing scenario
static void Main(string[] args)
{
ConfigureServices();
Console.ReadLine();
}
private static void ConfigureServices()
{
IServiceCollection serviceCollection = new ServiceCollection();
IServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
// Setup configuration service
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", true, true)
.AddEnvironmentVariables()
.Build();
serviceCollection.AddSingleton(configuration);
// Setup options
IConfiguration configurationFromDI = serviceProvider.GetService<IConfiguration>(); // This is just for testing purposes
IConfigurationSection myConfigurationSection = configurationFromDI.GetSection("app");
serviceCollection.AddSingleton<IValidateOptions<AppOptions>, AppOptionsValidator>();
serviceCollection.Configure<AppOptions>(myConfigurationSection);
// Try to read the current options
IOptions<AppOptions> appOptions = serviceProvider.GetService<IOptions<AppOptions>>();
Console.WriteLine(appOptions.Value.Foo);
}
Unfortunately the variable configurationFromDI is null. So the variable configuration wasn't added to the DI container.
How do I setup the Dependency Injection for console applications correctly?
The call to BuildServiceProvider should be made after all services are registered.
There's no need to write all of this code though. Since you use so many extensions already, it's better (and easier) to use the generic Host, the same way an ASP.NET Core application does and use its ConfigureServices, ConfigureAppConfiguration methods:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureHostConfiguration(configuration =>
{
configuration....;
});
.ConfigureServices((hostContext, services) =>
{
var myConfigurationSection = configuration.GetSection("app");
services.AddSingleton<IValidateOptions<AppOptions>, AppOptionsValidator>();
services.Configure<AppOptions>(myConfigurationSection);
});
}
Configuration is available through the HostBuilderContext.Configuration property.
CreateDefaultBuilder sets the current folder, configures environment variables and the use of appsettings.json files so there's no need to add them explicitly.
Appsettings.json copy settings
In a web app template, appsettings.json files are added automatically with the Build Action property set to Content and the Copy to Output action to Copy if Newer.
There are no such files in a Console app. When a new appsettings.json file is added by hand, its Build Action is None and Copy to Never. When the application is debugged the current directory is bin\Debug. With the default settings, appsettings.json won't be copied to bin/Debug
Build Action will have to change to Content and Copy should be set to Copy if Newer or Copy Always.
DI in Console project
You can setup DI in any executable .net-core app and configure services in a Startup class (just like web projects) by extending IHostBuilder:
public static class HostBuilderExtensions
{
private const string ConfigureServicesMethodName = "ConfigureServices";
public static IHostBuilder UseStartup<TStartup>(
this IHostBuilder hostBuilder) where TStartup : class
{
hostBuilder.ConfigureServices((ctx, serviceCollection) =>
{
var cfgServicesMethod = typeof(TStartup).GetMethod(
ConfigureServicesMethodName, new Type[] { typeof(IServiceCollection) });
var hasConfigCtor = typeof(TStartup).GetConstructor(
new Type[] { typeof(IConfiguration) }) != null;
var startUpObj = hasConfigCtor ?
(TStartup)Activator.CreateInstance(typeof(TStartup), ctx.Configuration) :
(TStartup)Activator.CreateInstance(typeof(TStartup), null);
cfgServicesMethod?.Invoke(startUpObj, new object[] { serviceCollection });
});
return hostBuilder;
}
}
Now, you have an extension method UseStartup<>() that can be called in Program.cs (the last line):
public class Program
{
public static void Main(string[] args)
{
// for console app
CreateHostBuilder(args).Build().Run();
// For winforms app (commented)
// Application.SetHighDpiMode(HighDpiMode.SystemAware);
// Application.EnableVisualStyles();
// Application.SetCompatibleTextRenderingDefault(false);
// var host = CreateHostBuilder(args).Build();
// Application.Run(host.Services.GetRequiredService<MainForm>());
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
config.AddJsonFile("appsettings.json", optional: false);
config.AddEnvironmentVariables();
// any other configurations
})
.UseStartup<MyStartup>();
}
Finally, Add your own Startup class (here, MyStartup.cs), and inject IConfiguration from constructor:
public class MyStartup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
// services.AddBlahBlahBlah()
}
}
P.S: you get null because you call .BuildServiceProvider before registering IConfiguration
Mapping appsettings.json
For mapping appsettings.json values to a type, define an empty interface like IConfigSection (just for generic constraints reason):
public interface IConfigSection
{
}
Then, extend IConfiguration interface like follow:
public static class ConfigurationExtensions
{
public static TConfig GetConfigSection<TConfig>(this IConfiguration configuration) where TConfig : IConfigSection, new()
{
var instance = new TConfig();
var typeName = typeof(TConfig).Name;
configuration.GetSection(typeName).Bind(instance);
return instance;
}
}
Extension methdod GetConfigSection<>() do the mapping for you. Just define your config classes that implement IConfigSection:
public class AppConfigSection : IConfigSection
{
public bool IsLocal { get; set; }
public bool UseSqliteForLocal { get; set; }
public bool UseSqliteForServer { get; set; }
}
Below is how your appsettings.json should look like (class name and property names should match):
{
.......
"AppConfigSection": {
"IsLocal": false,
"UseSqliteForServer": false,
"UseSqliteForLocal": false
},
.....
}
And finally, retrieve your settings and map them to your ConfigSections as follow:
// configuration is the injected IConfiguration
AppConfigSection appConfig = configuration.GetConfigSection<AppConfigSection>();

Getting Connection String in ASP.Net Core MVC data layer

I am using the code below to retrieve the connection string and it works fine. However, the configuration object has to be passed through the layers. Previous versions of .Net would allow me to get the connection string directly in the data layer. So can I still do that (and how do I do that) or do I need to pass the configuration object through the application as I do now?
In startup.cs
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton(_ => Configuration);
...
}
MyController.cs
public class MyController : Controller
{
protected readonly IConfiguration Configuration;
public MyController(IConfiguration configuration)
{
Configuration = configuration;
}
public IActionResult ListRecords()
{
DatabaseContext ctx = new DatabaseContext(Configuration);
return View();
}
}
DatabaseContext.cs
public class DatabaseContext : DbContext
{
private readonly IConfiguration config;
public DatabaseContext(IConfiguration config)
{
this.config = config;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(config["ConnectionStrings:Dev"]);
}
}
Having to explicitly inject IConfiguration is usually seen as a code smell and indicates design issues.
Take advantage of dependency injection
public class MyController : Controller {
DatabaseContext context;
public MyController(DatabaseContext context) {
this.context = context;
}
public IActionResult ListRecords() {
//...use context here
return View();
}
}
and inject the database options instead
public class DatabaseContext : DbContext {
public DatabaseContext(DbContextOptions<DatabaseContext> options): base(options) {
//...
}
}
Then it is only a matter of configuring the context at startup
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services) {
// ...
services.AddDbContext<DatabaseContext>(options =>
options.UseSqlServer(Congiguration.GetConnectionString("Dev"));
// ...
}
Typically the pattern I've used for setting up DBContext, is to configure at startup.
So if this is startup.cs:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
var sqlConnString = Configuration.GetConnectionString(dbConnectionStringSettingsName);
services.AddDbContext<DatabaseContext >(opt => opt.UseSqlServer(sqlConnString));
Also, if you pass your context as a service reference, you shouldn't need to give it IConfiguration.
private readonly DatabaseContext _context;
public MyController(DatabaseContext context)
{
_context = context;
}
public IActionResult ListRecords()
{
var dbresults = _context.Table.ToList();
return View(dbresults );
}
use nugget packaeges
Install-Package Microsoft.Extensions.Configuration.Abstractions
Install-Package Microsoft.Extensions.Configuration
and then Inject IConfigurationSection in the web application.
https://github.com/geeksarray/read-appsettings-json-in-net-core-class-library-using-dependency-injection

Xunit Testing EFcore Repositories InMemory DB

I am trying to unit test the repositories, I am using InMemory option in EFCore . This is the method
[Fact]
public async Task GetCartsAsync_Returns_CartDetail()
{
ICartRepository sut = GetInMemoryCartRepository();
CartDetail cartdetail = new CartDetail()
{
CommercialServiceName = "AAA"
};
bool saved = await sut.SaveCartDetail(cartdetail);
//Assert
Assert.True(saved);
//Assert.Equal("AAA", CartDetail[0].CommercialServiceName);
//Assert.Equal("BBB", CartDetail[1].CommercialServiceName);
//Assert.Equal("ZZZ", CartDetail[2].CommercialServiceName);
}
private ICartRepository GetInMemoryCartRepository()
{
DbContextOptions<SostContext> options;
var builder = new DbContextOptionsBuilder<SostContext>();
builder.UseInMemoryDatabase($"database{Guid.NewGuid()}");
options = builder.Options;
SostContext personDataContext = new SostContext(options);
personDataContext.Database.EnsureDeleted();
personDataContext.Database.EnsureCreated();
return new CartRepository(personDataContext);
}
I am getting error which say
System.TypeLoadException : Method 'ApplyServices' in type
'Microsoft.EntityFrameworkCore.Infrastructure.Internal.InMemoryOptionsExtension' from assembly
'Microsoft.EntityFrameworkCore.InMemory, Version=1.0.1.0, Culture=neutral,
PublicKeyToken=adb9793829ddae60' does not have an implementation.
Microsoft.
EntityFrameworkCore.InMemoryDbContextOptionsExtensions.UseInMemoryDatabase(DbContextOptionsBuilder
optionsBuilder, String databaseName, Action`1 inMemoryOptionsAction)
Microsoft.EntityFrameworkCore.InMemoryDbContextOptionsExtensions.UseInMemoryDatabase[TContext]
(DbContextOptionsBuilder`1 optionsBuilder, String databaseName, Action`1 inMemoryOptionsAction)
My reference is from https://www.carlrippon.com/testing-ef-core-repositories-with-xunit-and-an-in-memory-db/
Please suggest me where i am going wrong with the current implementation . Thanks in Advance
I suggest reading the official Microsoft documentation about integration testing.
https://learn.microsoft.com/fr-fr/aspnet/core/test/integration-tests?view=aspnetcore-3.0
Secondly, I you start adding this kind of boilerplate to create your tests with the memory database you will stop doing it very soon.
For integration tests, you should be near to your development configuration.
Here my configuration files and a usage in my CustomerController :
Integration Startup File
Have all think about database creation and dependency injection
public class IntegrationStartup : Startup
{
public IntegrationStartup(IConfiguration configuration) : base(configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public override void ConfigureServices(IServiceCollection services)
{
services.AddEntityFrameworkInMemoryDatabase().BuildServiceProvider();
services.AddDbContext<StreetJobContext>(options =>
{
options.UseInMemoryDatabase("InMemoryAppDb");
});
//services.InjectServices();
//here you can set your ICartRepository DI configuration
services.AddMvc(option => option.EnableEndpointRouting = false)
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.AddApplicationPart(Assembly.Load(new AssemblyName("StreetJob.WebApp")));
ConfigureAuthentication(services);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public override void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var serviceScopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using (var serviceScope = serviceScopeFactory.CreateScope())
{
//Here you can add some data configuration
}
app.UseMvc();
}
The fake startup
it's quite similar to the one in the Microsoft documentation
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
protected override IWebHostBuilder CreateWebHostBuilder()
{
return WebHost.CreateDefaultBuilder(null)
.UseStartup<TStartup>();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseSolutionRelativeContentRoot(Directory.GetCurrentDirectory());
builder.ConfigureAppConfiguration(config =>
{
config.AddConfiguration(new ConfigurationBuilder()
//custom setting file in the test project
.AddJsonFile($"integrationsettings.json")
.Build());
});
builder.ConfigureServices(services =>
{
});
}
}
The controller
public class CustomerControllerTest : IClassFixture<CustomWebApplicationFactory<IntegrationStartup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<IntegrationStartup> _factory;
private readonly CustomerControllerInitialization _customerControllerInitialization;
public CustomerControllerTest(CustomWebApplicationFactory<IntegrationStartup> factory)
{
_factory = factory;
_client = _factory.CreateClient();
}
}
With this kind of setting, testing the integration tests are very similar to the development controller.
It's a quite good configuration for TDD Developers.

Net Core: Execute All Dependency Injection in Xunit Test for AppService, Repository, etc

I am trying to implement Dependency Injection in Xunit test for AppService. Ideal goal is to run the original application program Startup/configuration, and use any dependency injection that was in Startup, instead of reinitializing all the DI again in my test, thats the whole Goal in question.
Update: Mohsen's answer is close. Need to update couple syntax/requirement errors to work.
For some reason, original application works and can call Department App Service. However, it cannot call in Xunit. Finally got Testserver working using Startup and Configuration from original application.
Now receiving error below:
Message: The following constructor parameters did not have matching fixture data: IDepartmentAppService departmentAppService
namespace Testing.IntegrationTests
{
public class DepartmentAppServiceTest
{
public DBContext context;
public IDepartmentAppService departmentAppService;
public DepartmentAppServiceTest(IDepartmentAppService departmentAppService)
{
this.departmentAppService = departmentAppService;
}
[Fact]
public async Task Get_DepartmentById_Are_Equal()
{
var options = new DbContextOptionsBuilder<SharedServicesContext>()
.UseInMemoryDatabase(databaseName: "TestDatabase")
.Options;
context = new DBContext(options);
TestServer _server = new TestServer(new WebHostBuilder()
.UseContentRoot("C:\\OriginalApplication")
.UseEnvironment("Development")
.UseConfiguration(new ConfigurationBuilder()
.SetBasePath("C:\\OriginalApplication")
.AddJsonFile("appsettings.json")
.Build()).UseStartup<Startup>());
context.Department.Add(new Department { DepartmentId = 2, DepartmentCode = "123", DepartmentName = "ABC" });
context.SaveChanges();
var departmentDto = await departmentAppService.GetDepartmentById(2);
Assert.Equal("123", departmentDto.DepartmentCode);
}
}
}
I am receiving this error:
Message: The following constructor parameters did not have matching fixture data: IDepartmentAppService departmentAppService
Need to use Dependency injection in testing just like real application.
Original application does this. Answers below are not currently sufficient , one uses mocking which is not current goal, other answer uses Controller which bypass question purpose.
Note: IDepartmentAppService has dependency on IDepartmentRepository which is also injected in Startup class, and Automapper dependencies. This is why calling the whole startup class.
Good Resources:
how to unit test asp.net core application with constructor dependency injection
Dependency injection in Xunit project
You are mixing unit test with integration test. TestServer is for integration test and if you want to reuse Startup class to avoid register dependencies again, you should use HttpClient and make HTTP call to controller and action that use IDepartmentAppService.
If you want do unit test, you need to setup DI and register all needed dependencies to test IDepartmentAppService.
Using DI through Test Fixture:
public class DependencySetupFixture
{
public DependencySetupFixture()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddDbContext<SharedServicesContext>(options => options.UseInMemoryDatabase(databaseName: "TestDatabase"));
serviceCollection.AddTransient<IDepartmentRepository, DepartmentRepository>();
serviceCollection.AddTransient<IDepartmentAppService, DepartmentAppService>();
ServiceProvider = serviceCollection.BuildServiceProvider();
}
public ServiceProvider ServiceProvider { get; private set; }
}
public class DepartmentAppServiceTest : IClassFixture<DependencySetupFixture>
{
private ServiceProvider _serviceProvide;
public DepartmentAppServiceTest(DependencySetupFixture fixture)
{
_serviceProvide = fixture.ServiceProvider;
}
[Fact]
public async Task Get_DepartmentById_Are_Equal()
{
using(var scope = _serviceProvider.CreateScope())
{
// Arrange
var context = scope.ServiceProvider.GetServices<SharedServicesContext>();
context.Department.Add(new Department { DepartmentId = 2, DepartmentCode = "123", DepartmentName = "ABC" });
context.SaveChanges();
var departmentAppService = scope.ServiceProvider.GetServices<IDepartmentAppService>();
// Act
var departmentDto = await departmentAppService.GetDepartmentById(2);
// Arrange
Assert.Equal("123", departmentDto.DepartmentCode);
}
}
}
Using dependency injection with unit test is not good idea and you should avoid that. by the way if you want don't repeat your self for registering dependencies, you can wrap your DI configuration in another class and use that class anywhere you want.
Using DI through Startup.cs:
public class IocConfig
{
public static IServiceCollection Configure(IServiceCollection services, IConfiguration configuration)
{
serviceCollection
.AddDbContext<SomeContext>(options => options.UseSqlServer(configuration["ConnectionString"]));
serviceCollection.AddScoped<IDepartmentRepository, DepartmentRepository>();
serviceCollection.AddScoped<IDepartmentAppService, DepartmentAppService>();
.
.
.
return services;
}
}
in Startup class and ConfigureServices method just useIocConfig class:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
IocConfig.Configure(services, configuration);
services.AddMvc();
.
.
.
if you don't want use IocConfig class, change ConfigureServices in Startup class:
public IServiceCollection ConfigureServices(IServiceCollection services)
{
.
.
.
return services;
and in test project reuse IocConfig or Startup class:
public class DependencySetupFixture
{
public DependencySetupFixture()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true));
configuration = builder.Build();
var services = new ServiceCollection();
// services = IocConfig.Configure(services, configuration)
// or
// services = new Startup(configuration).ConfigureServices(services);
ServiceProvider = services.BuildServiceProvider();
}
public ServiceProvider ServiceProvider { get; private set; }
}
and in test method:
[Fact]
public async Task Get_DepartmentById_Are_Equal()
{
using (var scope = _serviceProvider.CreateScope())
{
// Arrange
var departmentAppService = scope.ServiceProvider.GetServices<IDepartmentAppService>();
// Act
var departmentDto = await departmentAppService.GetDepartmentById(2);
// Arrange
Assert.Equal("123", departmentDto.DepartmentCode);
}
}
Use Custom Web Application Factory and ServiceProvider.GetRequiredService below, feel free to edit and optimize the answer
CustomWebApplicationFactory:
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((hostingContext, configurationBuilder) =>
{
var type = typeof(TStartup);
var path = #"C:\\OriginalApplication";
configurationBuilder.AddJsonFile($"{path}\\appsettings.json", optional: true, reloadOnChange: true);
configurationBuilder.AddEnvironmentVariables();
});
// if you want to override Physical database with in-memory database
builder.ConfigureServices(services =>
{
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
services.AddDbContext<ApplicationDBContext>(options =>
{
options.UseInMemoryDatabase("DBInMemoryTest");
options.UseInternalServiceProvider(serviceProvider);
});
});
}
}
Integration Test:
public class DepartmentAppServiceTest : IClassFixture<CustomWebApplicationFactory<OriginalApplication.Startup>>
{
public CustomWebApplicationFactory<OriginalApplication.Startup> _factory;
public DepartmentAppServiceTest(CustomWebApplicationFactory<OriginalApplication.Startup> factory)
{
_factory = factory;
_factory.CreateClient();
}
[Fact]
public async Task ValidateDepartmentAppService()
{
using (var scope = _factory.Server.Host.Services.CreateScope())
{
var departmentAppService = scope.ServiceProvider.GetRequiredService<IDepartmentAppService>();
var dbtest = scope.ServiceProvider.GetRequiredService<ApplicationDBContext>();
dbtest.Department.Add(new Department { DepartmentId = 2, DepartmentCode = "123", DepartmentName = "ABC" });
dbtest.SaveChanges();
var departmentDto = await departmentAppService.GetDepartmentById(2);
Assert.Equal("123", departmentDto.DepartmentCode);
}
}
}
Resources:
https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.2
https://fullstackmark.com/post/20/painless-integration-testing-with-aspnet-core-web-api
When you are testing. You need to use mocking libraries or Inject your service directly on contructor ie.
public DBContext context;
public IDepartmentAppService departmentAppService;
/// Inject DepartmentAppService here
public DepartmentAppServiceTest(DepartmentAppService departmentAppService)
{
this.departmentAppService = departmentAppService;
}

Categories

Resources