.NET How to access TOptions during startup (Options Pattern) - c#

I am using the .NET Options pattern to manage my configuration.
This configuration is needed in Controllers (easy with Dependency Injection) but also to configure other services during application startup.
I would have thought that the generic Services.Configure<MyOptionsClass> method would return an instance of MyOptionsClass but unfortunately it returns an IServiceCollection?
Is there a clean way to access a bound instance of MyOptionsClass here during startup?
var builder = WebApplication.CreateBuilder(args);
// Setup MyOptionsClass for DI
var unwantedServiceCollection = builder.Services.Configure<MyOptionsClass>(builder.Configuration.GetSection(MyOptionsClass.ConfigName));
// Already need to be able to access MyOptionsClass here:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => { options.Authority = instanceOfMyOptionsClass.Authority; });

I had a similar need in the past and this is what I did:
var configOptions = builder.Configuration.GetSection(MyOptionsClass.ConfigName);
//You can also add data annotations to your config validate it on start
builder.Services
.AddOptions<MyOptionsClass>()
.Bind(configOptions)
.ValidateDataAnnotations()
.ValidateOnStart();
var configInstance = configOptions.Get<MyOptionsClass>();
Alternatively, you can use ServiceProviderServiceExtensions GetService<> or GetRequiredService<> to get the service you need by its type. Also, please be wary of using BuildServiceProvider which may create duplicate services as mentioned here.
Hope this helps.

Related

C# >NET 7 Trying to resolve services (with a Singleton) prior to Service Provider being built

I'm using .NET 7 in a WebApplication with OAuth extensions.
The (GitHub) AuthenticationBuilder is added in the section of code before the application is built and the DI service provider is created.
However I need to work with a DI chain for UnitofWork/Repo/etc. Prior to the container being built. Based on some reading I decided to instantiate a premature copy of the container in my anonymous method for the oAuth event.
Once I did that the UI prompted me with a warning that a second container was being built and I get that totally kills the concept of a Singleton and probably isn't a really good idea.
Are there any suggestions on how to handle working with the DB from within the oAuth.OnCreatingTicket event? Or do I just need to pass configuration and manually instantiate all of the classes myself?
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddTransient<IUnitOfWork,UnitOfWork>();
builder.Services.AddSingleton<IDocumentStore>(s =>
DocumentStoreFactory.CreateStore(
builder.Configuration["Server"],
builder.Configuration["Database"]));
builder.Services.AddAuthentication(...)
.AddCookie(...)
.AddGitHub(o =>
{
o.ClientId = builder.Configuration["GitHub:ClientID"]!;
o.ClientSecret = builder.Configuration["GitHub:ClientSecret"]!;
o.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
//Building a temporary service provider so UnitOfWork/Service/DocumentStore DI chain can be fullfilled
var sp = builder.Services.BuildServiceProvider();
await OauthPersistUser.HandleGitHubOauthTicket(context, sp.GetService(typeof(ILogger<>)), sp.GetService<IUnitOfWork>());
}
};
});
var app = builder.Build();
//app Service provider Container is built and ready
Several hours of playing around later I realized that the context passed by the oAuth provider has access to the service provider via HttpContext.RequestServices.
so I can reduce my parameters to just the content, then grab the UnitOfWork object by:
public static async Task HandleGitHubOauthTicket(OAuthCreatingTicketContext context)
{
var uow = context.HttpContext.RequestServices.GetService<IUnitOfWork>();
var log = context.HttpContext.RequestServices.GetService<ILogger<OauthPersistUser>>();
...
}

MAUI dependency injection service resolve in Program.cs

MAUI has dependency injection setup similar to what ASP.NET Core has in the Startup.cs class. This one is set in MauiProgram.cs file by default.
My question is: How can I get a service instance in this file after services registration? I guess, one solution will be the following, but then I must edit this code also if the constrctors of these services change during time:
var keyValueStore = new PreferencesKeyValueStore();
var accountService = new AccountService(keyValueStore);
var profileService = new ProfileService(keyValueStore);
builder.Services.AddSingleton<IKeyValueStore>(keyValueStore);
builder.Services.AddSingleton<IAccountService>(accountService);
builder.Services.AddSingleton<IProfileService>(profileService);
//Here now I can use accountService and profileService to do something
I can not find more elegant solution that will return the service instance for me from the DI container. Something like:
builder.Services.AddSingleton<IKeyValueStore, PreferencesKeyValueStore>();
builder.Services.AddSingleton<IAccountService, AccountService>;
builder.Services.AddSingleton<IProfileService, ProfileService>();
//Now I can't perform something like: var accountService = diContainer.GetInstance<IAccountService>(); or similar.
I don't know how to reach di container and ask it to provide me registered instance.
Actually, the documentation provided a simple way to do so.
Check it here
They recommended to use the Handler property of any object of type Element, there you can write the code :
// Considering you want to resolve a service from your custom interface IMyService
var service = this.Handler.MauiContext.Services.GetService<IMyService>();
// Then you can use the resolved service..
But there are some issues, personally it never worked for me, the Handler property may be null because of the lifecycle of the Element you are calling it on.
To avoid this issue, use a full line like:
var service = Application.Current.MainPage
.Handler
.MauiContext
.Services
.GetService<IMyService>();
// Then you can use the resolved service..
This works fine for me
Hope it helps you ..

How to use a Service or DbContext inside DbCommandInterceptor?

I have an application that syncs data from a MySql database to a SQL Server database.
Considering those two DbContext services:
services.AddDbContext<SqlServerContext>(options => options
.UseSqlServer(Configuration.GetConnectionString("SqlServer")));
services.AddDbContext<MySqlContext>(options => options
.UseMySql(Configuration.GetConnectionString("MySql"))
.AddInterceptors(new MySqlInterceptor()));
In the MySqlInterceptor(); I want to inject/resolve/use a Service or even the SqlServerContext, in order to get configurations to modify the CommandText.
Any ideas?
Depending on the method you are going to override you will receive CommandEventData object in the method definition which has the DbContext as property.
As to the services and configurations you can configure the interceptor before registration.
Instead of this:
services.AddDbContext<MySqlContext>(options => options
.UseMySql(Configuration.GetConnectionString("MySql"))
.AddInterceptors(new MySqlInterceptor()));
you can do
var interceptor = new MySqlInterceptor(service1, service2 ... etc);
services.AddDbContext<MySqlContext>(options => options
.UseMySql(Configuration.GetConnectionString("MySql"))
.AddInterceptors(interceptor))
How to resolve the interceptor instance:
If you need to auto-wire the dependencies of the interceptor you can do the following
services.AddTransient<Service1>();
services.AddTransient<Service2>();
services.AddTransient<MySqlInterceptor>();
// resolve the instalce of the interceptor
var serviceProvider = services.BuildServiceProvider();
var interceptor = serviceProvider.GetService<MySqlInterceptor>();
// configure mysql context and interceptor
services.AddDbContext<MySqlContext>(options => options
.UseMySql(Configuration.GetConnectionString("MySql"))
.AddInterceptors(interceptor))
As #vasil mentioned in his answer:
Depending on the method you are going to override, you will receive
CommandEventData object in the method definition which has the
DbContext as property.
In my case though, I wanted to resolve a service that used another DbContext, which proved to be cumbersome; so instead I ended up putting the settings I needed into appsettings.json, and used IConfiguration service to get the setting value and sent it to the Interceptor constructor:
services.AddDbContext<MySqlContext>(options => options
.UseMySql(Configuration.GetConnectionString("MySql"))
.AddInterceptors(new MySqlInterceptor(Configuration["SettingValue"])));
Note: If you landed on this answer and was looking for a way to resolve a service inside the ConfigureService method, without calling BuildServiceProvider which, like David Fowler says on a Github issue, you'd be:
building the container while trying to build it
and you'll end up having:
2 containers and one of them will never be disposed.
You can do what Nkosi suggests in his answer:
services.AddScoped<IService>(x =>
new Service(x.GetRequiredService<IOtherService>(),
x.GetRequiredService<IAnotherOne>(),
""));

what is this ASP.NET Core log message: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager

I have this at every app start.
Does anyone know where this comes from?
info:
Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using '/Users/thomas/.aspnet/DataProtection-Keys' as key repository; keys
will not be encrypted at rest.
// run the web host
var PathToContentRoot = Directory.GetCurrentDirectory();
var Host = WebHost.CreateDefaultBuilder()
.UseKestrel()
.UseContentRoot(PathToContentRoot)
.UseStartup<WebStartup>()
.UseNLog()
.Build();
I don't have anything about 'dataprotection', 'keys', etc nor do I want any form of security features.
The code in the ConfigureServices part is:
// find all controllers
var Controllers =
from a in AppDomain.CurrentDomain.GetAssemblies().AsParallel()
from t in a.GetTypes()
let attributes = t.GetCustomAttributes(typeof(ControllerAttribute), true)
where attributes?.Length > 0
select new { Type = t };
var ControllersList = Controllers.ToList();
Logging.Info($"Found {ControllersList.Count} controllers");
// register them
foreach (var Controller in ControllersList)
{
Logging.Info($"[Controller] Registering {Controller.Type.Name}");
Services
.AddMvc()
.AddJsonOptions(Options => Options.SerializerSettings.ContractResolver = new DefaultContractResolver())
.AddApplicationPart(Controller.Type.Assembly);
}
// add signalR
Services.AddSignalR();
It is done to allow controllers from external assemblies to be used.
Depending on what ASP.NET features you are using, the Core Data Protection middleware may be setup and added into the dependency injection container.
This provides a mechanism for storing sensitive data. Depending on what environment you are running in this sensitive data will be stored in different locations. In your case you are getting the message that it is being stored in the user profile (a folder on the system) and in plain text (I'm assuming because you are running on Linux as they would by default get encrypted on Windows). This article has a nice description of the default location for storing the sensitive data.
In your case I suspect it is the use of SignalR that is causing the Core Data Protection middle ware to be added. Another common cause for it being added is calling
IServiceCollection.AddAuthentication

How to Inject AutoMapper as an instance using FreshMvvm and FreshIOC

I am trying to inject AutoMapper into my MainPageModel for my Xamarin.Forms app but it crashes when loading the app.
I am setting it up like this, first init the config and then pass the implementing type to the DI container.
// Init automapper
AutoMapper.Mapper.Initialize(cfg => cfg.AddProfile(new AppProfile()));
// Add automapper to DI
FreshMvvm.FreshIOC.Container.Register<IMapper, AutoMapper.Mapper>();
// Load page (results in crash)
var page = FreshPageModelResolver.ResolvePageModel<MainPageModel> ();
The error message I get is:
TinyIoCResolutionException: Resolve failed: IConfigurationProvider
If I try to inject a regular Service with some IService interface instead, that works perfectly fine so it seems to be an issue with how FreshMvvm instanstiates the AutoMapper instance.
In most examples I have seen with injecting AutoMapper with other DI-containers, an instance is first created together with some configuration and then added to the container. Like this for an regular dot net core app:
public void ConfigureServices(IServiceCollection services)
{
// Crate config instance
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new YourProfile());
});
// Create a mapper from config and add instance as singleton
services.AddSingleton<IMapper>(sp => config.CreateMapper())
}
According to the docs (https://github.com/rid00z/FreshMvvm#ioc-container-lifetime-registration-options) it seems FreshMvvm does not support adding instances though, and singletons are mapped like this:
FreshMvvm.FreshIOC.Container.Register<IService, MySingletonService>();
How can I inject AutoMapper with FreshMvvm? Do I need to create a DI mapping for the IConfigurationProvider provider as well? To which implementation if so?
The documentation is not obvious on this point. It is trivial once you understand how FreshIOC works. It accepts an instance as a parameter to the Register call.
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new AppProfile());
});
FreshMvvm.FreshIOC.Container.Register<IMapper>(config.CreateMapper());

Categories