Extension Method C# ASP.NET Core - c#

I have this extension method and I have a problem when I call it in my startup class:
public static class ServiceExtensions
{
public static void ConfigureMySqlContext(this IServiceCollection services, IConfiguration config)
{
var connectionString = config["mysqlconnection:connectionString"];
services.AddDbContext<RepositoryContext>(o => o.UseSqlServer(connectionString));
}
}
And this in startup class - I get an error to add IServiceCollection (.NET Core 6)
ServiceExtensions.ConfigureMySqlContext(builder.Configuration);

You are calling this extension in the wrong way. Since it is an "Extension" method, you need to invoke it on the concrete object, it is extending. By using the this keyword, you are extending the type you are invoking this method on by a specific functionality, without deriving from with a custom type.
services.ConfigureMySqlContext(builder.Configuration);
or, if you are basing your code off of the .net 6 minimal templates:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.ConfigureMySqlContext(builder.Configuration);
var app = builder.Build();
Documentation: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods

Related

Passing IOptions<T> to method in StartUp Configuration

I have set up my project to use the IOptions pattern for reading data from the appSettings file.
I have a class that has the following simple constructor to it:
public PlayClass(IOptions<MySettings> settings)
{
_settings = settings;
}
In my ConfigureServices method I have my config set up here:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MySettings>(options => Configuration.GetSection("MyOptions").Bind(options));
}
When I run or test this, everything works as expected. However, I need to call a method from my play PlayClass inside of ConfigureServices.
What is best way to achieve this?
I had originally thought it would be as simple as the following:
public void ConfigureServices(IServiceCollection services)
{
var x = services.Configure<BitBucketSettings>(options => Configuration.GetSection("BitBucketOptions").Bind(options));
var pc = new PlayClass(x);
pc.MyMethod();
}
But this only results in an error: cannot convert from IServiceCollection to IOptions<MySettings>
It is not clear why you want to create an object of class in Startup class. But you can solve your problem as following.
IServiceCollection is used only for create the dependency graph but to resolve the actual dependencies at runtime, ServiceProvider is needed.
To build ServiceProvider, BuildServiceProvider method needs to be called on ServiceCollection. You can see that in the code below.
public void ConfigureServices(IServiceCollection services)
{
//Register the configuration section in the service collection.
services.Configure<BitBucketSettings>(Configuration.GetSection("BitBucketOptions");
// Register the class in service collection.
services.AddScoped<PlayClass, PlayClass>();
// Build Service Provider
var sp = services.BuildServiceProvider();
// Resolve instance or PlayClass from service builder.
var pc = sp.GetService<PlayClass>();
// Call method on instance of PlayClass
pc.MyMethod();
}
I hope this will help you solve your issue.

Mapster Global Configuration with Dependency Injection

I'd like to know if there is a way to globally configure Mapster while using Dependency Injection?
The configuration options appear to be for the static usage and also for a singleton pattern only.
Mapster Configuration
Mapster Dependency Injection
I have created an extension method.
// Extension method
public static IServiceCollection AddMapster(this IServiceCollection services, Action<TypeAdapterConfig> options = null)
{
var config = new TypeAdapterConfig();
config.Scan(Assembly.GetAssembly(typeof(Startup)));
options?.Invoke(config);
services.AddSingleton(config);
services.AddScoped<IMapper, ServiceMapper>();
return services;
}
// Called in Startup.ConfigureServices(IServiceCollection services)
services.AddMapster(options =>
{
options.Default.IgnoreNonMapped(true); // Does not work.
TypeAdapterConfig.GlobalSettings.Default.IgnoreNonMapped(true); // Does not work.
});
I imagine these don't work because the ServiceMapper is creating its own instance without using anything I've configured.
I implemented Mapster in a Blazor Server application, and I struggled to find documentation on how to scan the assembly for mapping registrations.
I have a class in my application that implements the IRegister interface and defines the mappings
public class MappingRegistration : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
config.NewConfig<ModelA, ModelB>();
}
}
In the ConfigureServices of the Startup.cs I have this then
var typeAdapterConfig = TypeAdapterConfig.GlobalSettings;
// scans the assembly and gets the IRegister, adding the registration to the TypeAdapterConfig
typeAdapterConfig.Scan(Assembly.GetExecutingAssembly());
// register the mapper as Singleton service for my application
var mapperConfig = new Mapper(typeAdapterConfig);
services.AddSingleton<IMapper>(mapperConfig);
I hope this can save someone's time. If anybody is aware of better ways, please let me know.
You can change from
var config = new TypeAdapterConfig();
to
var config = TypeAdapterConfig.GlobalSettings;

How to utilize a required (scoped) service when using AddAuthentication during ConfigureServices in ASP.NET Core 3.0?

Inside my ASP.NET Core 3.0 ConfigureServices method (Startup class) I need to add authentication on a variety of third-party identity providers (IdPs) -- and I store that information in a database.
I add the database repository as a scoped service and then have defined an extension method that uses that repo to look up IdP configuration and add additional IdPs to the authentications of the applications.
The code below works, but I'm getting a warning about additional instances of singletons being created due to the use of BuildServiceProvider().
I'm wondering if there is a better approach for me to add these database-stored IdPs into the authentication mix without incurring these duplicates.
services.AddScoped<IIdentityServerRepository, IdentityServerRepository>();
var issuerUri = _config.GetValue<string>("MyIssuerUri");
var identityRepo = services.BuildServiceProvider().GetService<IIdentityServerRepository>();
services.AddAuthentication()
.AddIdentityProviders(identityRepo, issuerUri);
Here is some (not all) of the extension method class where AddIdentityProviders is defined:
public static class IdentityProviderHelper
{
public static AuthenticationBuilder AddIdentityProviders(this AuthenticationBuilder builder,
IIdentityServerRepository repo, string issuerUri)
{
builder.AddAzureAdProvider(repo);
builder.AddOpenIdConnectProviders(repo);
builder.AddSamlProviders(repo, issuerUri);
return builder;
}
private static AuthenticationBuilder AddAzureAdProvider(this AuthenticationBuilder builder, IIdentityServerRepository repo)
{
var azureConfig = repo.GetProviderConfigurationByName(ProviderEnum.AzureActiveDirectory).FirstOrDefault();
if (azureConfig == null) return builder;
return builder.AddOpenIdConnect(azureConfig.AuthenticationType, azureConfig.Description, options => SetOidcOptions(azureConfig, options));
}
...
}

Explicit Accessing Intance of Options and Passing it to Method within ConfigureServices with ASP.NET CORE API

I need to pass instance of IOptions to a method as parameter, like below: any idea?
services.SetWaitAndRetryPolicy<CustomHttpClient>(); //how to retrieve and pass instance of IOptions<MyConfig>
I follow the links below and at the bottom:
How to read AppSettings values from .json file in ASP.NET Core
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Add functionality to inject IOptions<T>
services.AddOptions();
// Add our Config object so it can be injected
services.Configure<MyConfig>(Configuration.GetSection("MyConfig"));
services.SetWaitAndRetryPolicy<CustomHttpClient>(); //how to retrieve and pass instance of IOptions<MyConfig>
}
public static class IServiceCollectionExtension
{
public static void SetWaitAndRetryPolicy<T>(this IServiceCollection services, IOptions<MyConfig> config) where T : class
{
}
}
How to get an instance of IConfiguration in asp.net core?
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-2.2
ASP.NET CORE 2.2
If you are using the extension method to register your CustomHttpClient class then you access the options within the configure method.
public static class IServiceCollectionExtension
{
public static void SetWaitAndRetryPolicy<T>(this IServiceCollection services) where T : class
{
services.AddHttpClient<T>((sp, client) =>
{
var options = sp.GetService<IOptions<MyConfig>>();
...
});
}
}
One of the parameters to the configure action is the IServiceProvider. From here you can get access to any of the registered services, in this case the IOptions<MyConfig> settings.

How to Unit Test Startup.cs in .NET Core

How do people go about Unit Testing their Startup.cs classes in a .NET Core 2 application? All of the functionality seems to be provided by Static extensions methods which aren't mockable?
If you take this ConfigureServices method for example:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<BlogContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddMvc();
}
How can I write tests to ensure that AddDbContext(...) & AddMvc() are called - the choice of implementing all of this functionality via extension methods seems to have made it untestable?
Well yes, if you want to check the fact that extension method AddDbContext was called on services you are in trouble.
The good thing is that you shouldn't actually check exactly this fact.
Startup class is an application composition root. And when testing a composition root you want to check that it actually registers all dependencies required for instantiation of the root objects (controllers in the case of ASP.NET Core application).
Say you have following controller:
public class TestController : Controller
{
public TestController(ISomeDependency dependency)
{
}
}
You could try checking whether Startup has registered the type for ISomeDependency. But implementation of ISomeDependency could also require some other dependencies that you should check.
Eventually you end up with a test that has tons of checks for different dependencies but it does not actually guarantee that object resolution will not throw missing dependency exception. There is not too much value in such a test.
An approach that works well for me when testing a composition root is to use real dependency injection container. Then I call a composition root on it and assert that resolution of the root object does not throw.
It could not be considered as pure Unit Test because we use other non-stubbed class. But such tests, unlike other integration tests, are fast and stable. And most important they bring the value of valid check for correct dependencies registration. If such test passes you could be sure that object will also be correctly instantiated in the product.
Here is a sample of such test:
[TestMethod]
public void ConfigureServices_RegistersDependenciesCorrectly()
{
// Arrange
// Setting up the stuff required for Configuration.GetConnectionString("DefaultConnection")
Mock<IConfigurationSection> configurationSectionStub = new Mock<IConfigurationSection>();
configurationSectionStub.Setup(x => x["DefaultConnection"]).Returns("TestConnectionString");
Mock<Microsoft.Extensions.Configuration.IConfiguration> configurationStub = new Mock<Microsoft.Extensions.Configuration.IConfiguration>();
configurationStub.Setup(x => x.GetSection("ConnectionStrings")).Returns(configurationSectionStub.Object);
IServiceCollection services = new ServiceCollection();
var target = new Startup(configurationStub.Object);
// Act
target.ConfigureServices(services);
// Mimic internal asp.net core logic.
services.AddTransient<TestController>();
// Assert
var serviceProvider = services.BuildServiceProvider();
var controller = serviceProvider.GetService<TestController>();
Assert.IsNotNull(controller);
}
I also had a similar problem, but managed to get around that by using the WebHost in AspNetCore and essentially re-creating what program.cs does, and then Asserting that all of my services exist and are not null. You could go a step further and execute specific extensions for IServices with .ConfigureServices or actually perform operations with the services you created to make sure they were constructed properly.
One key, is I created a unit test startup class that inherits from the startup class I'm testing so that I don't have to worry about separate assemblies. You could use composition if you prefer to not use inheritance.
[TestClass]
public class StartupTests
{
[TestMethod]
public void StartupTest()
{
var webHost = Microsoft.AspNetCore.WebHost.CreateDefaultBuilder().UseStartup<Startup>().Build();
Assert.IsNotNull(webHost);
Assert.IsNotNull(webHost.Services.GetRequiredService<IService1>());
Assert.IsNotNull(webHost.Services.GetRequiredService<IService2>());
}
}
public class Startup : MyStartup
{
public Startup(IConfiguration config) : base(config) { }
}
This approach works, and uses the real MVC pipeline, as things should only be mocked if you need to change how they work.
public void AddTransactionLoggingCreatesConnection()
{
var servCollection = new ServiceCollection();
//Add any injection stuff you need here
//servCollection.AddSingleton(logger.Object);
//Setup the MVC builder thats needed
IMvcBuilder mvcBuilder = new MvcBuilder(servCollection, new Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager());
IEnumerable<KeyValuePair<string, string>> confValues = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("TransactionLogging:Enabled", "True"),
new KeyValuePair<string, string>("TransactionLogging:Uri", "https://api.something.com/"),
new KeyValuePair<string, string>("TransactionLogging:Version", "1"),
new KeyValuePair<string, string>("TransactionLogging:Queue:Enabled", "True")
};
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(confValues);
var confRoot = builder.Build();
StartupExtensions.YourExtensionMethod(mvcBuilder); // Any other params
}
As an alternative approach to #datchung's answer with ASP.net Core 6 (or 7) Minimal start-up, it's possible to leverage WebApplicationFactory<T> to run startup. Note that this requires defining InternalsVisibleTo from API to test project for the Program reference to be accessible.
Sample test, using xUnit:
[Fact]
public void StartupTest()
{
var waf = new WebApplicationFactory<Program>();
var server = waf.Server;
// Optional: check for individual services
var myService = server.Services.GetService<IMyService>();
Assert.NotNull(myService);
}
The .Server call there triggers the test server and ServiceCollection build. That, in turn, triggers validation unless "ValidateOnBuild" option has been turned off.
More about WAF internals in here: https://andrewlock.net/exploring-dotnet-6-part-6-supporting-integration-tests-with-webapplicationfactory-in-dotnet-6/
All of this does require that your Startup code works in test scenario (it shouldn't connect to online services etc.) but that is also useful for integration testing too (e.g. Alba).
In my case, I'm using .NET 6 with the minimal API (no Startup class).
My Program.cs originally looked like this:
// using statements
...
var builder = WebApplication.CreateBuilder(args);
...
builder.services.AddSingleton<IMyInterface, MyImplementation>();
...
I added StartupHelper.cs:
public class StartupHelper
{
private readonly IServiceCollection _services;
public StartupHelper(IServiceCollection services)
{
_services = services;
}
public void SetUpServices()
{
_services.AddSingleton<IMyInterface, MyImplementation>();
}
}
I used StartupHelper in Program.cs:
// using statements
...
var builder = WebApplication.CreateBuilder(args);
...
var startupHelper = new StartupHelper(builder.Services);
startupHelper.SetUpServices();
...
And my test (NUnit) looks like this:
[Test]
public void SetUpServices()
{
var builder = WebApplication.CreateBuilder(new string[0]);
var startupHelper = new StartupHelper(builder.Services);
startupHelper.SetUpServices();
var app = builder.Build();
var myImplementation = app.Services.GetService<IMyInterface>();
Assert.NotNull(myImplementation);
Assert.IsTrue(myImplementation is MyImplementation);
}
You should be install to Xunit project then add startup.cs file in base directory .

Categories

Resources