Using a razor pages app and .NET 6, how does this look for encrypting/decrypting data? I did a bit of research and apparently AES-CBC encryption isn't recommended, and this was the alternative I found (AES_256_GCM). Also, is there a clean way to turn this into a library that can be reused in other versions of .NET (large environment, it'll take time to upgrade everything)?
It's close to other posts about using it in .NET core, but some minor (yet "it won't work without it") tweaks:
Program.cs file:
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDataProtection();
builder.Services.AddDataProtection()
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration()
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_GCM,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});
builder.Services.AddSingleton<CipherService>();
var app = builder.Build();
Class for the cipher:
using Microsoft.AspNetCore.DataProtection;
namespace Encryption.BusinessLogic
{
public class CipherService
{
private readonly IDataProtectionProvider _dataProtectionProvider;
private const string Key = "my-very-long-key-of-no-exact-size";
public CipherService(IDataProtectionProvider dataProtectionProvider)
{
_dataProtectionProvider = dataProtectionProvider;
}
public string Encrypt(string input)
{
var protector = _dataProtectionProvider.CreateProtector(Key);
return protector.Protect(input);
}
public string Decrypt(string cipherText)
{
var protector = _dataProtectionProvider.CreateProtector(Key);
return protector.Unprotect(cipherText);
}
}
}
Code behind on the index page:
private readonly ILogger<IndexModel> _logger;
private readonly IDataProtectionProvider _dataProtectionProvider;
[BindProperty]
public string InputText { get; set; }
[BindProperty]
public string Enc { get; set; }
public IndexModel(ILogger<IndexModel> logger, IDataProtectionProvider dataProtectionProvider)
{
_logger = logger;
_dataProtectionProvider = dataProtectionProvider;
}
public void OnGet()
{
}
public void OnPost()
{
CipherService cipher = new CipherService(_dataProtectionProvider);
Enc = cipher.Encrypt(InputText);
}
I've taken a different approach by using this article, https://code-maze.com/data-protection-aspnet-core/. I've made a minor change. On the controller: I've added the following declaration to deal with Route Constraints. So far it's working for me. This is for .Net Core.
[Route("Controller/Action/{id:regex(^[[a-zA-Z0-9_-]]*$)}")] // matches alpha, number, plus _ and +
public async Task<IActionResult> EditPermission(string id)
{
`var decryptedId = Convert.ToInt32(_dataProtector.Unprotect(id));
}
Related
Information
I am building an Azure hosted protected API. I am targeting .Net 5 and using the MVC pattern. Inside this API, I have two DAL (data access layer) classes, which are added to the service container via the Startup.cs file. Both of these classes are Transient and expect a connection string in their constructors.
Problem
I want to move the connection strings from the appsetting.json file of the API to the Azure key vault, but I only want to have to pull down the connection strings once from the Azure key vault and use them as static values to initialize any instances of the DAL classes. I've tried calling the respective method of the SecretClient class called GetSecretAsync and I use the constructor of SecretClient(Uri, ManagedIdentityCredential). This all works when I call this after Startup.cs has ran, but when I try to do it inside of the Startup.cs it seems to hang for some unknown reason. I know that the calls do work later in the pipeline as I've tested them in the API as well as in the Azure Portal. This is what I've been trying to do via code:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//For Production, set the properties of the KeyVault
Secrets.SetProps(new KeyVaultHelper(Configuration)).GetAwaiter().GetResult();
//Get the AzureAd section
try
{
//Instead of storing the AzureAdBinding info in appsettings, I created a
//json string which deserializes into a Dictionary which is used to
//create the ConfigurationSection object
Dictionary<string, string> azureadkvps = JsonConvert.DeserializeObject<Dictionary<string, string>>(Secrets.AzureAdBinding);
//Turn it into an IConfigurationSection
IConfigurationSection azureconfigsection = new ConfigurationBuilder().AddInMemoryCollection(azureadkvps).Build().GetSection("AzureAd");
//Bind authentication to Configuration AzureAd section
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(azureconfigsection);
}
catch { }
Secrets.cs
/// <summary>
/// Static class to hold values of keys pulled from Azure key vault
/// </summary>
public static class Secrets
{
private const string MOBILE_APP_ID = "xxxxxxxxxx";
private const string B_DB_CONN = "xxxxxxxxxx";
private const string MOBILE_ADMIN_ROLE = "xxxxxxxxxx";
private const string MOBILE_API_SCOPES = "xxxxxxxxxx";
private const string MOBILE_COMMON_ROLE = "xxxxxxxxxx";
private const string BL_DB_CONN = "xxxxxxxxxx";
private const string AZUREAD_BINDING = "xxxxxxxxxx";
public static string BLConn { get; private set; } = null;
public static string BConn { get; private set; } = null;
public static string MobileAdminRole { get; private set; } = null;
public static string MobileUserRole { get; private set; } = null;
public static string MobileApiScopes { get; private set; } = null;
public static string MobileClientID { get; private set; } = null;
public static string AzureAdBinding { get; private set; } = null;
public async static Task SetProps(IKeyVaultHelp keyvault)
{
Task<string>[] secretretrievaltasks = new Task<string>[7];
secretretrievaltasks[0] = keyvault.GetSecretAsync(MOBILE_APP_ID);
secretretrievaltasks[1] = keyvault.GetSecretAsync(B_DB_CONN);
secretretrievaltasks[2] = keyvault.GetSecretAsync(MOBILE_ADMIN_ROLE);
secretretrievaltasks[3] = keyvault.GetSecretAsync(MOBILE_API_SCOPES);
secretretrievaltasks[4] = keyvault.GetSecretAsync(MOBILE_COMMON_ROLE);
secretretrievaltasks[5] = keyvault.GetSecretAsync(BL_DB_CONN);
secretretrievaltasks[6] = keyvault.GetSecretAsync(AZUREAD_BINDING);
await Task.WhenAll(secretretrievaltasks).ConfigureAwait(false);
BLConn = secretretrievaltasks[5].Result;
BConn = secretretrievaltasks[1].Result;
MobileAdminRole = secretretrievaltasks[2].Result;
MobileUserRole = secretretrievaltasks[4].Result;
MobileApiScopes = secretretrievaltasks[3].Result;
MobileClientID = secretretrievaltasks[0].Result;
AzureAdBinding = secretretrievaltasks[6].Result;
}
}
KeyVaultHelper.cs
public class KeyVaultHelper : IKeyVaultHelp
{
private readonly IConfiguration _config;
public KeyVaultHelper(IConfiguration config)
{
_config = config;
}
public string GetSecret(string secretkey)
{
string kvuri = $"https://{_config.GetValue<string>("KVN")}.vault.azure.net";
var secretclient = new SecretClient(new Uri(kvuri), new ManagedIdentityCredential("xxxxxxxxxxxxxxxxxxxx"));
return secretclient.GetSecret(secretkey).Value.Value;
}
public async Task<string> GetSecretAsync(string secretkey)
{
string kvuri = $"https://{_config.GetValue<string>("KVN")}.vault.azure.net";
var secretclient = new SecretClient(new Uri(kvuri), new ManagedIdentityCredential());
return (await secretclient.GetSecretAsync(secretkey).ConfigureAwait(false)).Value.Value;
}
}
Perhaps I am going about this all wrong, but I'd like to be able to pull these values before adding the DAL classes to the service container so that the static values of the Secrets.cs hold the correct connection strings which will be used to construct the DAL objects.
I understand there is a way to add a Configuration file in Azure which can be built in Program.cs via the following, but I was hoping to just pull the values directly from KeyVault and use them throughout the lifetime of the application. Any direction or advice is appreciated.
webBuilder.ConfigureAppConfiguration((hostingContext, config) =>
{
var settings = config.Build();
config.AddAzureAppConfiguration(options =>
{
options.Connect(settings["ConnectionStrings:AppConfig"])
.ConfigureKeyVault(kv =>
{
kv.SetCredential(new DefaultAzureCredential());
});
});
})
.UseStartup<Startup>());
}
Thanks Camilo Terevinto, your comments are converting to an answer. So that it may helps to other community members to find this correct solution.
Instead in the Startup, use await with async Task Main in Program class where you already have the IConfiguration configured.
use AddAzureAppConfiguration if you're using app services, or
put the secrets in environment variables if you're running on VMs/containers
To populate the static values before the call to CreateHostBuilder(args).Build().Run(), you should call the async or await method.
I want to add two or more(depends on how many azure storage container i want to add to my app) services in Startup.cs
My appsettings.json:
"AzureBlobStorageConfiguration": {
"Storages": {
"Storage1": {
"StorageName": "Storage1",
"ConnString": "connString",
"AzureBlobContainerName": "containerName"
},
"Storage2": {
"StorageName": "Storage2",
"ConnString": "connString",
"AzureBlobContainerName": "containerName"
},
"Storage3": {
"StorageName": "Storage3",
"ConnString": "connString",
"AzureBlobContainerName": "containerName"
}
}
Next in Startup.cs im adding service with method:
public static IServiceCollection AddAzureStorage1(this IServiceCollection services, IConfiguration configuration)
{
var options = new ABlobStorageConfigurationOptionsDTO();
configuration.GetSection("AzureBlobStorageConfiguration").GetSection("Storages").GetSection("Storage1").Bind(options);
services.AddTransient<IAzureBlobStorage1, AzureBlobStorage1>(isp =>
{
var client = new BlobServiceClient(options.ConnString);
var container = client.GetBlobContainerClient(options.AzureBlobContainerName);
var containerName = options.AzureBlobContainerName;
var storageName = options.StorageName;
return new AzureBlobStorage1(container, containerName, storageName);
}
);
return services;
}
My IAzureBlobStorage1 looks like:
public interface IAzureBlobStorage1
{
string AzureBlobContainerName { get; }
string StorageName { get; }
public Task<Stream> DownloadStreamAsyns(string fileName);
public Task Upload(string fileId, Stream stream);
}
and AzureBlobStorage1 :
public class AzureBlobStorage1 : IAzureBlobStorage1
{
private BlobContainerClient _client;
private string _containerName;
private string _storageName;
public string StorageName => _storageName;
public string AzureBlobContainerName => _containerName;
public AzureBlobStorage1(BlobContainerClient client, string containerName, string storageName)
{
_client = client;
_containerName = containerName;
_storageName = storageName;
}
public async Task<Stream> DownloadStreamAsyns(string fileName)
{
return await _client.GetBlobClient(fileName).OpenReadAsync();
}
public async Task Upload(string fileId, Stream stream)
{
await _client.GetBlobClient(fileId).UploadAsync(stream);
}
}
After this i can injection interface in my constructor controller class :
public Controller(IAzureBlobStorage1 azureStorage)
{
_azureStorage1 = azureStorage;
}
But if i want to add many storages (i have 3 in appsetings.json) i have to:
Create interface IAzureBlobStorage2 (looking the same like IAzureBlobStorage1 - only name change)
Create class AzureBlobStorage2 (looking the same like AzureBlobStorage1 - only name change)
copy-paste method with changed class names
public static IServiceCollection AddAzureStorage2(this IServiceCollection services, IConfiguration configuration)
{
var options = new ABlobStorageConfigurationOptionsDTO();
configuration.GetSection("AzureBlobStorageConfiguration").GetSection("Storages").GetSection("Storage2").Bind(options);
services.AddTransient<IAzureBlobStorage2, AzureBlobStorage2>(isp =>
{
var client = new BlobServiceClient(options.ConnString);
var container = client.GetBlobContainerClient(options.AzureBlobContainerName);
var containerName = options.AzureBlobContainerName;
var storageName = options.StorageName;
return new AzureBlobStorage2(container, containerName, storageName);
}
);
return services;
}
Now i can get it in controller by
public Controller(IAzureBlobStorage2 azureStorage)
{
_azureStorage2 = azureStorage;
}
If i want add my third storage i need to copy-paste third time my code.
For me this solution looks very bad and im thinking how i can resolve it and make my code clean.
Unsure if this is a best practice or not, but you could design a named service provider, maybe? Either that, or you could just a generic parameter to differentiate them, but that generic parameter wouldn't mean much except as a way to differentiate..
Anyways, here's a really basic implementation using some kind of named provider?:
public interface INamedService {
string Identifier { get; }
}
public interface IAzureBlobStorage : INamedService
{
string AzureBlobContainerName { get; }
string StorageName { get; }
Task<Stream> DownloadStreamAsyns(string fileName);
Task Upload(string fileId, Stream stream);
}
public class NamedServiceProvider<T>
where T : INamedService
{
readonly IReadOnlyDictionary<string, T> Instances;
public NamedServiceProvider(
IEnumerable<T> instances)
{
Instances = instances?.ToDictionary(x => x.Identifier) ??
throw new ArgumentNullException(nameof(instances));
}
public bool TryGetInstance(string identifier, out T instance) {
return Instances.TryGetValue(identifier, out instance);
}
}
public class AzureBlobStorage : IAzureBlobStorage
{
public string Identifier { get; }
private BlobContainerClient _client;
private string _containerName;
private string _storageName;
public string StorageName => _storageName;
public string AzureBlobContainerName => _containerName;
public AzureBlobStorage(string identifier, BlobContainerClient client, string containerName, string storageName)
{
Identifier = identifier;
_client = client;
_containerName = containerName;
_storageName = storageName;
}
public async Task<Stream> DownloadStreamAsyns(string fileName)
{
return await _client.GetBlobClient(fileName).OpenReadAsync();
}
public async Task Upload(string fileId, Stream stream)
{
await _client.GetBlobClient(fileId).UploadAsync(stream);
}
}
And then the static extension method:
public static IServiceCollection AddAzureStorage(
this IServiceCollection services,
IConfiguration configuration,
string identifier)
{
var options = new ABlobStorageConfigurationOptionsDTO();
configuration
.GetSection("AzureBlobStorageConfiguration")
.GetSection("Storages")
.GetSection(identifier)
.Bind(options);
return services
.TryAddTransient<NamedServiceProvider<IAzureBlobStorage>>()
.AddTransient<IAzureBlobStorage, AzureBlobStorage>(isp =>
{
var client = new BlobServiceClient(options.ConnString);
var container = client.GetBlobContainerClient(options.AzureBlobContainerName);
var containerName = options.AzureBlobContainerName;
var storageName = options.StorageName;
return new AzureBlobStorage(identifier, container, containerName, storageName);
});
}
Then you could call use it like so:
public Controller(NamedServiceProvider<IAzureBlobStorage> azureStorage)
{
_ = azureStorage ?? throw new ArgumentNullException(nameof(azureStorage));
_azureStorage2 = azureStorage.TryGetInstance("Storage2", out var instance) ? instance : throw new Exception("Something about the identifier not being found??");
}
I coded this outside of an intellisense environment, so sorry if there are any smaller mispellings or bugs. There may be a better way to do this, but this seemed at least somewhat ok-ish? Oh, and I only changed what I had to in order to make it work generically. I didn't want to touch any other logic..
i use Mediator in my project .
Demo Project on Github
i want to use TDD in my project and integration test with .Net core 3.0
i write this code int test class for use intergration test with mediator :
public class SubscribeTest : IClassFixture<TravelRequest<Startup>>, IClassFixture<DbContextFactory>, IDisposable
{
private readonly TravelRequest<Startup> request;
private readonly DbContextFactory contextFactory;
public SubscribeTest(TravelRequest<Startup> request , DbContextFactory contextFactory)
{
this.request = request;
this.contextFactory = contextFactory;
}
public void Dispose()
{
request.Dispose();
}
[Fact]
public async Task ListSubscribeAsync()
{
var add = await request.Get("/Subscribe/GetListSubscribe");
await add.BodyAs<SubscribListDto>();
}
}
and this is TravelRequest :
public class TravelRequest<TStartup> : IDisposable where TStartup : class
{
private readonly HttpClient client;
private readonly TestServer server;
public TravelRequest()
{
var webHostBuilder = new WebHostBuilder().UseStartup<TStartup>().UseConfiguration(ConfigorationSingltonConfigoration.GetConfiguration());
this.server = new TestServer(webHostBuilder);
this.client = server.CreateClient();
}
}
and this is ConfigorationSingltonConfigoration for use the appSetting-test.json :
public class ConfigorationSingltonConfigoration
{
private static IConfigurationRoot configuration;
private ConfigorationSingltonConfigoration() { }
public static IConfigurationRoot GetConfiguration()
{
if (configuration is null)
configuration = new ConfigurationBuilder()
.SetBasePath(Path.Combine(Path.GetFullPath("../../../")))
.AddJsonFile("appsettings-test.json")
.AddEnvironmentVariables()
.Build();
return configuration;
}
}
and finally this is for set DbContext :
public class DbContextFactory : IDisposable
{
public TravelContext Context { get; private set; }
public DbContextFactory()
{
var dbBuilder = GetContextBuilderOptions<TravelContext>("SqlServer");
Context = new TravelContext(dbBuilder.Options);
Context.Database.Migrate();
}
public void Dispose()
{
Context.Dispose();
}
public TravelContext GetRefreshContext()
{
var dbBuilder = GetContextBuilderOptions<TravelContext>("SqlServer");
Context = new TravelContext(dbBuilder.Options);
return Context;
}
private DbContextOptionsBuilder<TravelContext> GetContextBuilderOptions<T>(string connectionStringName)
{
var connectionString = ConfigorationSingltonConfigoration.GetConfiguration().GetConnectionString(connectionStringName);
var contextBuilder = new DbContextOptionsBuilder<TravelContext>();
var servicesCollection = new ServiceCollection().AddEntityFrameworkSqlServer().BuildServiceProvider();
contextBuilder.UseSqlServer(connectionString).UseInternalServiceProvider(servicesCollection);
return contextBuilder;
}
}
Now my problem is here , when i RunTest in result it show me this error :
---- System.InvalidOperationException : Unable to resolve service for type 'System.Collections.Generic.IList1[FluentValidation.IValidator1[Travel.Services.SubscribeServices.Query.SubscribeList.SubscribeListCommand]]' while attempting to activate 'Travel.Common.ValidateBehavior.ValidateCommandBehavior2[Travel.Services.SubscribeServices.Query.SubscribeList.SubscribeListCommand,Travel.Common.Operation.OperationResult1[System.Collections.Generic.IEnumerable`1[Travel.ViewModel.SubscibeDto.SubscribListDto]]]'. ---- System.NotImplementedException : The method or operation is not implemented.
whats the problem ? how can i solve ths problem ??? i put the source code of project in top of question
I suspect that the validators are not registered into the container.
You can use the FluentValidation in .NET Core with Dependency Injection, by installing the nuget and than passing the assembly.
More references at Using FluentValidation in .NET Core with Dependency Injection.
More reference of how you can do this at Fluent Validation on .NET Core 2.0 does not register.
So I have a .NET Core 2.2 app running on an Azure VM with Windows Server 2019 which has the following disk configuration:
The disk on the red box is where the App files are located. When the configuration file is updated either programatically or manually, IOptionsMonitor<T> is not picking up the changes.
As stated in this link:
As mentioned in the documentation, just enabling reloadOnChange and then injecting IOptionsSnapshot<T> instead of IOptions<T> will be enough. That requires you to have properly configured that type T though.
Which I did, as shown in this code:
private IConfiguration BuildConfig()
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("Config.json", false, reloadOnChange: true)
.Build();
}
public async Task MainAsync()
{
AppDomain.CurrentDomain.ProcessExit += ProcessExit;
...
IServiceCollection services = ConfigureServices();
// Configures the writable options from https://github.com/Nongzhsh/Awesome.Net.WritableOptions
services.ConfigureWritableOptions<ConfigurationSettings>(_config.GetSection("configurationSettings"), "ConfigDev.json");
// ConfigurationSettings is the POCO representing the config.json contents.
services.Configure<ConfigurationSettings>(_config.GetSection("configurationSettings"));
...
}
I haven't implemented the OnChange method since I'm assuming that the values should be automatically updated once the file's contents have changed. I have also tried setting the .NET Core's DOTNET_USE_POLLING_FILE_WATCHER to true but it did not work.
Here's is my code for reading and writing values to the configuration file:
public TimeService(
IServiceProvider provider,
IWritableOptions<ConfigurationSettings> writeOnlyOptions,
IOptionsMonitor<ConfigurationSettings> hotOptions)
{
_provider = provider;
_writeOnlyOptions = writeOnlyOptions;
_hotOptions = hotOptions;
}
private async Task EnsurePostedGameSchedules()
{
DateTime currentTime = DateTime.Now;
...
# region [WINDOWS ONLY] Lines for debugging.
// _hotOptions is the depency-injected IOptionsMonitor<T> object.
if (ConnectionState == ConnectionState.Connected)
{
await debugChannel.SendMessageAsync(
embed: RichInfoHelper.CreateEmbed(
"What's on the inside?",
$"Connection State: {ConnectionState}{Environment.NewLine}" +
$"Last Message ID: {_hotOptions.CurrentValue.LatestScheduleMessageID}{Environment.NewLine}" +
$"Last Message Timestamp (Local): {new ConfigurationSettings { LatestScheduleMessageID = Convert.ToUInt64(_hotOptions.CurrentValue.LatestScheduleMessageID) }.GetTimestampFromLastScheduleMessageID(true)}{Environment.NewLine}" +
$"Current Timestamp: {DateTime.Now}",
"").Build());
}
#endregion
if (new ConfigurationSettings { LatestScheduleMessageID = _hotOptions.CurrentValue.LatestScheduleMessageID }.GetTimestampFromLastScheduleMessageID(true).Date != currentTime.Date &&
currentTime.Hour >= 1)
{
...
try
{
...
if (gameScheds?.Count > 0)
{
if (gameSchedulesChannel != null)
{
// The line below updates the configuration file.
_writeOnlyOptions.Update(option =>
{
option.LatestScheduleMessageID = message?.Id ?? default;
});
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message + Environment.NewLine + e.StackTrace);
}
}
}
And here's the config POCO:
public class ConfigurationSettings
{
public string Token { get; set; }
public string PreviousVersion { get; set; }
public string CurrentVersion { get; set; }
public Dictionary<string, ulong> Guilds { get; set; }
public Dictionary<string, ulong> Channels { get; set; }
public ulong LatestScheduleMessageID { get; set; }
public string ConfigurationDirectory { get; set; }
public DateTime GetTimestampFromLastScheduleMessageID(bool toLocalTime = false) =>
toLocalTime ?
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000).ToLocalTime() :
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000);
}
Is there anything that I still need to do in order for IOptionsMonitor<T> to pick up the config changes in the config file?
EDIT: I forgot to tell how I configured the entire app. The program by the way is a long-running .NET Core console app (not a web app) so this is how the entire program is configured:
using ...
namespace MyProject
{
public class Program
{
static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
variables...
public async Task MainAsync()
{
AppDomain.CurrentDomain.ProcessExit += ProcessExit;
_client = new DiscordSocketClient();
_config = BuildConfig();
IServiceCollection services = ConfigureServices();
services.ConfigureWritableOptions<ConfigurationSettings>(_config.GetSection("configurationSettings"), "Config.json");
services.Configure<ConfigurationSettings>(_config.GetSection("configurationSettings"));
IServiceProvider serviceProvider = ConfigureServiceProvider(services);
serviceProvider.GetRequiredService<LogService>();
await serviceProvider.GetRequiredService<CommandHandlingService>().InitializeAsync(_config.GetSection("configurationSettings"));
serviceProvider.GetRequiredService<TimeService>().Initialize(_config.GetSection("configurationSettings"));
await _client.LoginAsync(TokenType.Bot, _config.GetSection("configurationSettings")["token"]);
await _client.StartAsync();
_client.Ready += async () =>
{
...
};
await Task.Delay(-1);
}
private void ProcessExit(object sender, EventArgs e)
{
try
{
...
}
catch (Exception ex)
{
...
}
}
private IServiceCollection ConfigureServices()
{
return new ServiceCollection()
// Base Services.
.AddSingleton(_client)
.AddSingleton<CommandService>()
// Logging.
.AddLogging()
.AddSingleton<LogService>()
// Extras. Is there anything wrong with this?
.AddSingleton(_config)
// Command Handlers.
.AddSingleton<CommandHandlingService>()
// Add additional services here.
.AddSingleton<TimeService>()
.AddSingleton<StartupService>()
.AddTransient<ConfigurationService>();
}
public IServiceProvider ConfigureServiceProvider(IServiceCollection services) => services.BuildServiceProvider();
private IConfiguration BuildConfig()
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("Config.json", false, true)
.Build();
}
}
}
It now worked without adding anything. I just let the app run using the compiled executable when I let my project target .NET Core 3.1. The app before was targeting .NET Core 2.2 and ran via PowerShell. I have no idea PowerShell has issues with IOptionsMonitor<T>.
According to my test, if we want to use IOptionsMonitor<T> to pick up the config changes in the config file, please refer to the following steps
My config.json
{
"configurationSettings": {
"Token": "...",
"PreviousVersion": "145.8.3",
"CurrentVersion": "145.23.4544",
"Guilds": {
"this setting": 4
},
"Channels": {
"announcements": 6
},
"LatestScheduleMessageID": 456,
"ConfigurationDirectory": "test"
}
}
My POCO
public class MyOptions
{
public string Token { get; set; }
public string PreviousVersion { get; set; }
public string CurrentVersion { get; set; }
public Dictionary<string, ulong> Guilds { get; set; }
public Dictionary<string, ulong> Channels { get; set; }
public ulong LatestScheduleMessageID { get; set; }
public string ConfigurationDirectory { get; set; }
public DateTime GetTimestampFromLastScheduleMessageID(bool toLocalTime = false) =>
toLocalTime ?
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000).ToLocalTime() :
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000);
}
Defile a class to save changes
public interface IWritableOptions<out T> : IOptions<T> where T : class, new()
{
void Update(Action<T> applyChanges);
}
public class WritableOptions<T> : IWritableOptions<T> where T : class, new()
{
private readonly IHostingEnvironment _environment;
private readonly IOptionsMonitor<T> _options;
private readonly string _section;
private readonly string _file;
public WritableOptions(
IHostingEnvironment environment,
IOptionsMonitor<T> options,
string section,
string file)
{
_environment = environment;
_options = options;
_section = section;
_file = file;
}
public T Value => _options.CurrentValue;
public T Get(string name) => _options.Get(name);
public void Update(Action<T> applyChanges)
{
var fileProvider = _environment.ContentRootFileProvider;
var fileInfo = fileProvider.GetFileInfo(_file);
var physicalPath = fileInfo.PhysicalPath;
var jObject = JsonConvert.DeserializeObject<JObject>(File.ReadAllText(physicalPath));
var sectionObject = jObject.TryGetValue(_section, out JToken section) ?
JsonConvert.DeserializeObject<T>(section.ToString()) : (Value ?? new T());
applyChanges(sectionObject);
jObject[_section] = JObject.Parse(JsonConvert.SerializeObject(sectionObject));
File.WriteAllText(physicalPath, JsonConvert.SerializeObject(jObject, Formatting.Indented));
}
}
Implemented an extension method for ServiceCollectionExtensions allowing you to easily configure a writable options
public static class ServiceCollectionExtensions
{
public static void ConfigureWritable<T>(
this IServiceCollection services,
IConfigurationSection section,
string file = "appsettings.json") where T : class, new()
{
services.Configure<T>(section);
services.AddTransient<IWritableOptions<T>>(provider =>
{
var environment = provider.GetService<IHostingEnvironment>();
var options = provider.GetService<IOptionsMonitor<T>>();
return new WritableOptions<T>(environment, options, section.Key, file);
});
}
}
Please add the following code in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("Config.json", optional: false, reloadOnChange:true);
var config = configBuilder.Build();
services.ConfigureWritable<MyOptions>(config.GetSection("configurationSettings"));
...
}
Change the Json vaule
private readonly IWritableOptions<Locations> _writableLocations;
public OptionsController(IWritableOptions<Locations> writableLocations)
{
_writableLocations = writableLocations;
}
//Update LatestScheduleMessageID
public IActionResult Change(string value)
{
_writableLocations.Update(opt => {
opt.LatestScheduleMessageID = value;
});
return Ok("OK");
}
Read the JSON value
private readonly IOptionsMonitor<MyOptions> _options;
public HomeController(ILogger<HomeController> logger, IHostingEnvironment env, IOptionsMonitor<MyOptions> options)
{
_logger = logger;
_env = env;
_options = options;
}
public IActionResult Index()
{
var content= _env.ContentRootPath;
var web = _env.WebRootPath;
#ViewBag.Message = _options.CurrentValue.LatestScheduleMessageID;
return View();
}
Result
First
After change:
In one of my concrete class. I have the method.
public class Call : ICall
{
......
public Task<HttpResponseMessage> GetHttpResponseMessageFromDeviceAndDataService()
{
var client = new HttpClient();
var uri = new Uri("http://localhost:30151");
var response = GetAsyncHttpResponseMessage(client, uri);
return response;
}
Now I put the url into appsettings.json.
{
"AppSettings": {
"uri": "http://localhost:30151"
}
}
And I created a Startup.cs
public class Startup
{
public IConfiguration Configuration { get; set; }
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json");
Configuration = builder.Build();
}
}
and now I get stuck.
EDIT
By the way, I don't have a controller, it is a console application.
The preferred way to read configuration from appSettings.json is using dependency injection and the built or (or 3rd party) IoC container. All you need is to pass the configuration section to the Configure method.
public class AppSettings
{
public int NoRooms { get; set; }
public string Uri { get; set; }
}
services.Configure<AppSettings>(Configuration.GetSection("appsettings"));
This way you don't have to manually set the values or initialize the AppSettings class.
And use it in your service:
public class Call : ICall
{
private readonly AppSettings appSettings;
public Call(IOptions<AppSettings> appSettings)
{
this.appSettings = appSetings.Value;
}
public Task<HttpResponseMessage>GetHttpResponseMessageFromDeviceAndDataService()
{
var client = new HttpClient();
var uri = new Uri(appSettings.Uri);
var response = GetAsyncHttpResponseMessage(client, uri);
return response;
}
}
The IoC Container can also be used in a console application, you just got to bootstrap it yourself. The ServiceCollection class has no dependencies and can be instantiated normally and when you are done configuring, convert it to an IServiceProvider and resolve your main class with it and it would resolve all other dependencies.
public class Program
{
public static void Main(string[] args)
{
var configurationBuilder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json");
var configuration = configurationBuilder.Build()
.ReloadOnChanged("appsettings.json");
var services = new ServiceCollection();
services.Configure<AppSettings>(configuration.GetSection("appsettings"));
services.AddTransient<ICall, Call>();
// add other services
// after configuring, build the IoC container
IServiceProvider provider = services.BuildServiceProvider();
Program program = provider.GetService<Program>();
// run the application, in a console application we got to wait synchronously
program.Wait();
}
private readonly ICall callService;
// your programs main entry point
public Program(ICall callService)
{
this.callService = callService;
}
public async Task Run()
{
HttpResponseMessage result = await call.GetHttpResponseMessageFromDeviceAndDataService();
// do something with the result
}
}
Create a static class
public static class AppSettings
{
public static IConfiguration Configuration { get; set; }
public static T Get<T>(string key)
{
if (Configuration == null)
{
var builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
var configuration = builder.Build();
Configuration = configuration.GetSection("AppSettings");
}
return (T)Convert.ChangeType(Configuration[key], typeof(T));
}
}
then access the settings anywhere you want like
var uri = AppSettings.Get<string>("uri");
var rooms = AppSettings.Get<int>("noRooms");
appsettings.json example
{
"AppSettings": {
"uri": "http://localhost:30151",
"noRooms": 100
}
}
You can access data from the IConfigurationRoot as following:
Configuration["AppSettings:uri"]
Like suggested in the comment I would put the information in a seperate class for that info and pass it into the DI container.
the class
public class AppSettings {
public string Uri { get; set; }
}
DI
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppSettings>(new AppSettings() { Uri = Configuration["AppSettings:uri"] });
// ...
}
Controller
public class DemoController
{
public HomeController(IOptions<AppSettings> settings)
{
//do something with it
}
}