Custom IConfiguration Service in Client SIde Blazor - c#

I am trying to have some basic configuration from json file to a singleton service inside my client side blazor application at the start up.
Below is my code setup
AppConfig and IAppConfig files
interface IAppConfig
{
string BaseUrl { get; set; }
string Source { get; set; }
}
and
public class AppConfig : IAppConfig
{
public string BaseUrl { get; set; }
public string Source { get; set; }
}
Than a json file by the name of environment.json inside wwwroot as wwwroot/ConfigFiles/environment.json
Than a service to read this file
interface ISharedServices
{
Task<AppConfig> GetConfigurationAsync();
}
and
public class SharedServices : ISharedServices
{
private HttpClient Http { get; set; }
public SharedServices(HttpClient httpClient)
{
Http = httpClient;
}
public async Task<AppConfig> GetConfigurationAsync()
{
return await Http.GetJsonAsync<AppConfig>("ConfigFiles/environment.json");
}
}
Now i am calling it into my component which load first
public class IndexComponent : ComponentBase
{
[Inject]
internal IAppConfig AppConfig { get; set; }
[Inject]
internal ISharedServices sharedServices { get; set; }
protected override async Task OnInitializedAsync()
{
var appconfig = await sharedServices.GetConfigurationAsync();
AppConfig = appconfig;
}
}
All this works fine , but i want to have this configuration ready at the time of application load in browser , so as suggested by "auga from mars" in my other Question i tried below code inside startup.cs at the moment i add IAppConfig as singleton service
services.AddSingleton<IAppConfig, AppConfig>(provider =>
{
var http = provider.GetRequiredService<HttpClient>();
return http.GetJsonAsync<AppConfig>("ConfigFiles/environment.json").GetAwaiter().GetResult();
});
But , buy using this code the blazor app never start up , all it show a blank white page with text Loading.... , not even any error but in every 5 min pop up show - page taking too much time to load with two option of wait and close .
If i change this code a bit from
return http.GetJsonAsync<AppConfig>("ConfigFiles/environment.json").GetAwaiter().GetResult();
to
return http.GetJsonAsync<AppConfig>("ConfigFiles/environment.json").Result;
Than it say - "Maximum call stack size exceed"
How to have configuration ready at startup ??
Update 1:
A little Update
in Basecomponent file , code is
protected override async Task OnInitializedAsync()
{
var appconfig = await sharedServices.GetConfigurationAsync();
AppConfig.BaseUrl = appconfig.BaseUrl;
AppConfig.Source = appconfig.Source;
}
I have to set every property one by one manually , need to get rid of this too

Related

Blazor | GetFromJsonAsyn error net::ERR_FAILED 200

I'm tryting to to the following:
[Inject] public HttpClient HttpClient { get; set; } = default!;
public AzureActiveDirectoryUser[] AzureActiveDirectoryUsers { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
var authenticationState = await authenticationStateTask;
if (authenticationState.User?.Identity?.IsAuthenticated == true)
{
AzureActiveDirectoryUsers = await HttpClient.GetFromJsonAsync<AzureActiveDirectoryUser[]>(url); //this is line 52
}
}
But I'm getting the following error:
I don't really understand what it means.
What I'm trying to get is a JSON with usernames and emails (I have access to the URL and can verify that the data is there)
AzureActiveDirectoryUsers is just a class with an id and a mail.
(edit)
I used to have this error. But it just suddenly stopped.
In OnInitializedAsync not everything is initalized on the page.
Move the code in OnAfterRenderAsync and call StateHasChanged if needed.

C# Injecting Specific Item from appSettings.json at runtime

How can I inject a specific setting (of possibly many) from an array appSettings.json in a C# .NET Core Web API, based on a runtime input value?
appSettings.json:
{
"SettingProfiles": [
{
"Name": "Profile1",
"SettingA": "SettingAValue1",
"SettingB": "SettingBValue1"
},
{
"Name": "Profile2",
"SettingA": "SettingAValue2",
"SettingB": "SettingBValue2"
}
...
}
Settings Classes:
public class Settings {
public List<SettingsProfile> SettingsProfiles { get; set; }
}
public class SettingsProfile {
public string Name { get; set; };
public string SettingA { get; set; };
public string SettingB { get; set; };
}
Service class:
public class MyService : IMyService {
private readonly SettingsProfile _Profile;
public MyService(SettingsProfile profile) {
_Profile = profile;
}
public void DoStuff() {
Console.WriteLine($"Setting A: {_SettingsProfile.SettingA}, Setting B: {_SettingsProfile.SettingB}")
}
}
The user will enter the setting name they want to apply. I am unsure how to do this if the service is configured in Startup.cs, at which point I don't yet have the setting to use.
I am understanding that "newing" the service would be bad practice, although that's the only way I can figure out how to make it work:
public class MyController {
private readonly Settings _Settings;
public MyController(Settings settings) {
_Settings = settings;
}
public IActionResult DoStuff(profileName) {
SettingsProfile profile = _Settings.Where(profile => profile.Name == profileName);
MyService service = new Service(profile);
}
}
I'm obviously missing something, but I've been watching YouTube videos on Dependency Injections and reading StackOverflow until my eyes bleed, and haven't figured it out yet. Can someone help me with a pattern that I should be following?
This is how I think it should work.
It will be a lot cleaner if you use another pattern: Factory.
interface ISettingServiceFactory{
MyService GetService(string profileName);
}
class SettingServiceFactory: ISettingServiceFactory
{
MyService GetService(string profileName){
}
}
Now you can implement GetService in two ways.
The first one is by creating new as you did in the controller and is not that bad as this is the purpose of the factory. In this way you kind of move that logic somewhere else.
A second one would be a bit uglier but something like this
interface ISettingServiceFactory{
MyService GetService(string profileName);
void SetCurrentProfile(SettingsProfile profile);
}
class SettingServiceFactory: ISettingServiceFactory
{
private IServiceProvider _serviceProvider;
private Settings _Settings;
public SettingServiceFactory(IServiceProvider serviceProvider,Settings settings){
_serviceProvider = serviceProvider;
_Settings = settings;
}
MyService GetService(string profileName){
var service = _serviceProvider.GetRequiredService<MyService>();
var profile = _Settings.Where(profile => profile.Name == profileName);
service.SetCurrentProfile(profile);
return service;
}
}
This second approach would be useful only if the implementation of MyService has a lot of other dependencies by itself and if you want to avoid new at any cost.
In both cases you will inject the factory in the controller
public MyController(ISettingServiceFactory settingServiceFactory) {
_settingServiceFactory= settingServiceFactory;
}
public IActionResult DoStuff(profileName) {
MyService service = _settingServiceFactory.GetService(profileName)
}

Is it possible in Blazor Server to inject scoped service in middleware and inject that same instance in component?

The application is Blazor Server and the question is very similar to Scope in Middleware and Blazor Component but it has not been active for long and I've clarified a few parts.
I've written a middleware that reads a cookie from the current request. A scoped service has been injected (singleton is not suitable since it's per user) via InvokeAsync and it gets updated with a value from the cookie. The same service is injected in a page component but unfortunately it's not the same instance of the service.
I've tried both render-mode="Server" and render-mode="ServerPrerendered". They both behave differently as you would expect but nevertheless it is not the same instance as the one created in the middleware. In render-mode Server the service is injected once as you expected and in render-mode ServerPrerendered the service is injected twice, once for the prerendered page and once for the interactive page. My goal is to have the same scoped service injected for the request in the middleware to also be injected in the page component. Is this possible?
Code for adding middleware (a bit simplified but still same problem). I've added some filtering since I'm only interested in the page request:
app.UseWhen(
context =>
{
return (context.Request.Path.StartsWithSegments("/_content") ||
context.Request.Path.StartsWithSegments("/_framework") ||
context.Request.Path.StartsWithSegments("/_blazor") ||
context.Request.Path.StartsWithSegments("/images") ||
context.Request.Path.StartsWithSegments("/favicon.ico") ||
context.Request.Path.StartsWithSegments("/css")) == false;
}
, builder => builder.UseSettingsMiddleware());
Adding the scoped service:
public void ConfigureServices(IServiceCollection services)
{
/* all other services added before this */
services.AddScoped<IThemeService, ThemeService>();
}
The middleware:
public class ThemeMiddleware
{
private readonly RequestDelegate _next;
private string _id;
public ThemeMiddleware(RequestDelegate next)
{
_next = next;
_id = Guid.NewGuid().ToString()[^4..];
}
public async Task InvokeAsync(HttpContext httpContext, IThemeService themeService)
{
var request = httpContext.Request;
string path = request.Path;
string theme = request.Cookies["App.Theme"];
Debug.WriteLine($"Middleware [{_id}]: Service [{themeService.GetId()}] | Request Path={path} | Theme={theme}");
if(string.IsNullOrEmpty(theme) == false)
{
themeService.SetTheme(theme);
}
await _next(httpContext);
}
}
The service:
public class ThemeService : IThemeService, IDisposable
{
string _theme = "default";
string _id;
string dateTimeFormat = "ss.fffffff";
public ThemeService()
{
_id = Guid.NewGuid().ToString()[^4..];
}
public void Dispose() { }
public string GetId() { return _id; }
public string GetTheme()
{
Debug.WriteLine($"ThemeService [{_id}]: GetTheme={DateTime.Now.ToString(dateTimeFormat)}");
return _theme;
}
public void SetTheme(string theme)
{
Debug.WriteLine($"ThemeService [{_id}]: SetTheme={DateTime.Now.ToString(dateTimeFormat)}");
_theme = theme;
}
}
The component (basically same code also exists in MainLayout.razor):
#page "/"
#inject IThemeService ThemeService
#code {
protected override async Task OnInitializedAsync()
{
System.Diagnostics.Debug.WriteLine($"Index.razor: Service [{ThemeService.GetId()}]");
}
}
Output
render-mode=Server
Middleware [399d]: Service [1f37] | Request Path=/ | Theme=dark
ThemeService [1f37]: SetTheme=00.5996142
MainLayout.razor: Service [4e96]
ThemeService [4e96]: GetTheme=01.0375910
Index.razor: Service [4e96]
render-mode=ServerPrerendered
Middleware [982d]: Service [5fa8] | Request Path=/ | Theme=dark
ThemeService [5fa8]: SetTheme=03.2477461
MainLayout.razor: Service [5fa8]
ThemeService [5fa8]: GetTheme=03.3576799
Index.razor: Service [5fa8]
MainLayout.razor: Service [d27c]
ThemeService [d27c]: GetTheme=03.9510551
Index.razor: Service [d27c]
The service id is actually the same in the prerendered request but not in the interactive one which is the one that counts. Any ideas on how to move forward?
I have seen a similar problem and was able to solve it as follows:
Prepare a wrapper class similar to the one below:
public static class Wrapper<T>
{
private static AsyncLocal<T> _value = new AsyncLocal<T>();
public static T CurrentValue
{
get
{
return _value.Value;
}
set
{
_value.Value = value;
}
}
}
prepare a middleware:
public class TestMiddleware<T>: IMiddleware
{
public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
Wrapper<T>.CurrentValue = /* set references */;
await next(context);
}
}
You may now access the Wrapper<T>.CurrentValue from a Blazor page or a scoped / transient service which was instantiated in the current Blazor circuit.
The root cause is, if I remember correctly by looking in the source code, that the Blazor DI scope is a new instance and not the middleware DI scope.
I opted for another solution to the initial problem, i.e. read a cookie-value and have it available in an injected service on the component-level. I would have preferred to handle this as a middleware, but unfortunately never found a way to do this except when using a singleton service and that is not an option.
Startup.cs:
services.AddScoped<IThemeService, ThemeService>();
ThemeService.cs:
public class ThemeService : IThemeService, IDisposable
{
string _id;
string _theme = "default";
string _themeCookieName;
public ThemeService(IHttpContextAccessor contextAccessor, IOptions<MyConfiguration> myConfig)
{
_id = Guid.NewGuid().ToString()[^4..];
_themeCookieName = myConfig.Value.ThemeCookieName;
RetrieveTheme(contextAccessor);
}
void RetrieveTheme(IHttpContextAccessor contextAccessor)
{
var request = contextAccessor.HttpContext.Request;
string theme = request.Cookies[_themeCookieName];
if (string.IsNullOrEmpty(theme) == false)
{
_theme = theme;
}
}
public void Dispose() { }
public string GetId() { return _id; }
public string GetTheme() { return _theme; }
public void SetTheme(string theme) { _theme = theme; }
}
MainLayout.razor:
#inject IThemeService ThemeService
/* markup */
#code {
bool _darkTheme = false;
MudTheme _theme = new DefaultTheme();
protected override async Task OnInitializedAsync()
{
var currentTheme = ThemeService.GetTheme();
_darkTheme = currentTheme == "dark";
_theme = _darkTheme ? new DarkTheme() : new DefaultTheme();
}
}

How to acess the appsettings in blazor webassembly

I currentying trying to save the api url in an appsettings.
However, the configuration.Propertiers seems to be empty. I am not sure how to get the setting.
in program.cs:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
//string url = builder.Configuration.Properties["APIURL"].ToString();
foreach (var prop in builder.Configuration.Properties)
Console.WriteLine($"{prop.Key} : {prop.Value}" );
//builder.Services.AddSingleton<Service>(new Service(url));
builder.RootComponents.Add<App>("app");
await builder.Build().RunAsync();
}
Inkkiller nailed it. You can simplify the call into IConfiguration without the APIHelper class and access it directly in Program.cs from the WebAssemblyHostBuilder.
appsettings:
{
"ServerlessBaseURI": "http://localhost:0000/",
}
Program.cs:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
string serverlessBaseURI = builder.Configuration["ServerlessBaseURI"];
}
This answer concerned blazor preview when blazor didn't support appsettings.json in wwwroot folder yet. You should use appsettings.json in wwroot folder now and WebAssemblyHostBuilder.Configuration. It also support per environment files (appsettings.{env}.Json).
I solve this issue by using a settings.json file store in the app wwwroot folder and register a task to get the settings :
Settings.cs
public class Settings
{
public string ApiUrl { get; set; }
}
wwwroot/settings.json
{
"ApiUrl": "https://localhost:51443/api"
}
Progam.cs
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddSingleton(async p =>
{
var httpClient = p.GetRequiredService<HttpClient>();
return await httpClient.GetJsonAsync<Settings>("settings.json")
.ConfigureAwait(false);
});
SampleComponent.razor
#inject Task<Settings> getsettingsTask
#inject HttpClient client
...
#code {
private async Task CallApi()
{
var settings = await getsettingsTask();
var response = await client.GetJsonAsync<SomeResult>(settings.ApiUrl);
}
}
This has advantages:
Doesn't share the server's appsettings.json file which can be a security hole
Configurable per environment
Using ASP.NET Core 6.0 Blazor configuration. Blazor WebAssembly loads configuration from the following app settings files by default:
wwwroot/appsettings.json.
wwwroot/appsettings.{ENVIRONMENT}.json, where the {ENVIRONMENT}
placeholder is the app's runtime environment.
Example:
wwwroot/appsettings.json
{
"h1FontSize": "50px"
}
Pages/ConfigurationExample.razor
#page "/configuration-example"
#using Microsoft.Extensions.Configuration
#inject IConfiguration Configuration
<h1 style="font-size:#Configuration["h1FontSize"]">
Configuration example
</h1>
Warning Configuration and settings files in a Blazor WebAssembly app
are visible to users. Don't store app secrets, credentials, or any
other sensitive data in the configuration or files of a Blazor
WebAssembly app.
https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/configuration?view=aspnetcore-6.0
You can also bind the values to a class.
public class ClientAppSettings
{
public string h1FontSize{ get; set; }
}
Then add this class as a Singleton in Program.cs:
var settings = new ClientAppSettings();
builder.Configuration.Bind(settings);
builder.Services.AddSingleton(settings);
Add namespace to _Imports.razor and then inject where needed to get settings with autocomplete in Visual Studio:
#inject ClientAppSettings ClientAppSettings
You can also just (appsettings.json in wwwroot):
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
var url = builder.Configuration.GetValue<string>("ApiConfig:Url");
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(url) });
}
}
As of now, you can use the IConfiguration.
appsettings.json:
{
"Services": {
"apiURL": "https://localhost:11111/"
}
}
.
using Microsoft.Extensions.Configuration;
public class APIHelper
{
private string apiURL;
public APIHelper(IConfiguration config)
{
apiURL = config.GetSection("Services")["apiURL"];
//Other Stuff
}
}
Blazor WASM appsettings.json
If you dont have appsettings.json in the wwwroot folder then simply:
Right click on wwwroot folder.
Click Add ==> New Item ==> App Settings File
This will add appsettings.json to your application. Open the appsettings.json file you will see a section in it already for database add a section like I have added apiinfo:
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=_CHANGE_ME;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"apiinfo":{
"apiurl": "your api url"
}
}
Now when you want to call this section simply Inject configuration and call it like:
#inject Microsoft.Extensions.Configuration.IConfiguration config;
And to call the apiurl:
config.GetSection("apiinfo")["apiurl"].ToString()
as an example, I have it implemeneted like this (client-side Blazor):
appsettings.json:
{
"api": "https://www.webapiurl.com/"
"ForceHTTPS": false
}
then, have typed config class
public class APISetting
{
public string api { get; set; }
public bool ForceHTTPS { get; set; }
}
then, load on startup:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(GetConfiguration());
}
public void Configure(IComponentsApplicationBuilder app )
{
app.AddComponent<App>("app");
}
public APISetting GetConfiguration()
{
using (var stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("appsettings.json"))
using (var reader = new System.IO.StreamReader(stream))
{
return System.Text.Json.JsonSerializer.Deserialize<APISetting>(reader.ReadToEnd());
}
}
}
Also in .Net 5 & 6 you can set the value to Static Class.
Example:
wwwroot/appsettings.json
"ServicesUrlOptions": {
"Url": "https://domain.gr/services" }
Static Class
public static class ApplicationServicesSettings
{
public const string ServicesUrl = "ServicesUrlOptions";
public static ServicesUrlOptions ServicesUrlOptions { get; set; } = new ServicesUrlOptions();
}
public class ServicesUrlOptions
{
public string Url { get; set; }
}
Finally bind the value at Program.cs
builder.Configuration.GetSection(ApplicationServicesSettings.ServicesUrl).Bind(ApplicationServicesSettings.ServicesUrlOptions);
After in project you have access to key by
ApplicationServicesSettings.ServicesUrlOptions.Url
create settings class:
public class Settings
{
public string ApiUrl { get; set; }
}
create settings.json in wwwroot folder:
{
"ApiUrl": "http://myapiurlhere"
}
and in .razor component read it like this:
#inject HttpClient Http
...
#code {
private string WebApuUrl = "";
protected override async Task OnInitializedAsync()
{
var response = await Http.GetFromJsonAsync<Settings>("settings.json");
WebApuUrl = response.ApiUrl;
}
}

How to add Bing Spellcheck to LuisRecognizerMiddleware?

Ok so this is how my LUIS app is configured in my bot.
On the LUIS website I can add Bing Spell Check to correct common spelling mistakes and have a better intent and entity match.
All that is required is that a BING API key needs to be added to the LUIS query string. But where do I configure that in the LuisRecognizerMiddleware?
I'm not even sure if that's the right place. But I guess it does put together the URI.
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddBot<MyBot>(options =>
{
options.CredentialProvider = new ConfigurationCredentialProvider(_configuration);
options.Middleware.Add(new CatchExceptionMiddleware<Exception>(async (context, exception) =>
{
await context.TraceActivity("MyBotException", exception);
await context.SendActivity("Sorry, it looks like something went wrong!");
}));
IStorage dataStore = new MemoryStorage();
options.Middleware.Add(new ConversationState<MyBotConversationState>(dataStore));
// Add LUIS recognizer as middleware
// see https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs
(string modelId, string subscriptionKey, Uri url) = GetLuisConfiguration(_configuration);
LuisModel luisModel = new LuisModel(modelId, subscriptionKey, url);
options.Middleware.Add(new LuisRecognizerMiddleware(luisModel));
});
}
private static (string modelId, string subscriptionKey, Uri url) GetLuisConfiguration(IConfiguration configuration)
{
string modelId = configuration.GetSection("Luis-ModelId")?.Value;
string subscriptionKey = configuration.GetSection("Luis-SubscriptionId")?.Value;
string url = configuration.GetSection("Luis-Url")?.Value;
Uri baseUri = new Uri(url);
return (modelId, subscriptionKey, baseUri);
}
All I get so far is...
GET https://westeurope.api.cognitive.microsoft.com/luis/v2.0/apps/?subscription-key=&q=test234&log=True HTTP/1.1
What I expect is something among those lines (copied from the LUIS web portal)
GET https://westeurope.api.cognitive.microsoft.com/luis/v2.0/apps/?subscription-key=&spellCheck=true&bing-spell-check-subscription-key=&verbose=true&timezoneOffset=0&q=test234
I just had a quick glimpse at the source code and figured ILuisOptions is what I am looking for. There was no concrete implementation to that. It's "roll your own" I guess...
public class MyLuisOptions : ILuisOptions
{
public bool? Log { get; set; }
public bool? SpellCheck { get; set; }
public bool? Staging { get; set; }
public double? TimezoneOffset { get; set; }
public bool? Verbose { get; set; }
public string BingSpellCheckSubscriptionKey { get; set; }
}
...and of course you have to pass this along to the LuisRecognizerMiddleware.
options.Middleware.Add(new LuisRecognizerMiddleware(luisModel, new LuisRecognizerOptions { Verbose = true }, new MyLuisOptions { SpellCheck = true, BingSpellCheckSubscriptionKey = "test123" }));

Categories

Resources