Writing to .config files - asp.net-mvc

I am having an issue somewhat similar to this question: C#: Config file error
BACKGROUND
My situation is this: I am using NLog and I have setup a Database target. My app shows an installation page on first run from which I build a connection string for other purposes but would also like to save that same connection string to the NLog.config file. After much searching it would appear that NLog can be changed programatically, but for whatever reason cannot save the changes back to the .config file. Therefore, my plan was to simply do the following:
WHAT I HAVE TRIED
Change the connectionString attribute to simply be connectionStringName instead. Example: connectionStringName="NLogConnection"
Move the <connectionStrings> element from my Web.config to a separate file: ConnectionStrings.config.
Update web.config accordingly:
<connectionStrings configSource="ConnectionStrings.config" />
Write code as follows:
string filePath = HostingEnvironment.MapPath(Path.Combine(httpRequest.ApplicationPath, "ConnectionStrings.config"));
var fileMap = new ExeConfigurationFileMap
{
ExeConfigFilename = filePath
};
var configFile = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
configFile.ConnectionStrings.ConnectionStrings["NLogConnection"].ConnectionString = connectionString;
configFile.Save();
OUTCOME
Seemed like a nice plan. However.. the code above throws an error about not having a <configuration> tag and if I add that tag, the site throws an error about the tag being there! No win situation here... does anyone have any ideas on how to solve this one? Ideally it would be nice if there was a way to properly modify external .config files which are only a part of the main web config. I may have to revert to manually reading/writing the raw XML.. but I would prefer not to if there's a better/more elegant way.
SOLUTION
Many thanks to #Julian; I now have a working solution. I added this in my Global.asax:
private static void TryUpdateNLogConnectionString(string connectionString)
{
try
{
var target = LogManager.Configuration.FindTargetByName("database");
DatabaseTarget databaseTarget = null;
var wrapperTarget = target as WrapperTargetBase;
// Unwrap the target if necessary.
if (wrapperTarget == null)
{
databaseTarget = target as DatabaseTarget;
}
else
{
databaseTarget = wrapperTarget.WrappedTarget as DatabaseTarget;
}
databaseTarget.ConnectionString = connectionString;
}
catch { }
}

You can change settings programmatically in NLog, but you can't serialize those settings to the XML, yet.
What you can do:
Save the setting to the <appSettings> when changed
read the <appSettings> when needed.
eg
using System.Web.Configuration;
using System.Configuration;
Configuration config = WebConfigurationManager.OpenWebConfiguration("/");
config.AppSettings.Settings["NlogConnectionString"].Value = "NewValue";
config.Save(ConfigurationSaveMode.Modified);
Edit the connectionstring when needed in NLog:
DatabaseTarget databaseTarget = LogManager.Configuration.FindTargetByName(targetName) as DatabaseTarget;
databaseTarget.ConnectionString = System.Configuration.ConfigurationManager.AppSettings["NlogConnectionString"];

Related

How to dynamically add a controller in a ASP.NET Core 6 MVC application

I need to dynamically creates controllers in a ASP.NET Core 6 MVC application.
I found some way to somewhat achieve this but not quite.
I'm able to dynamically add my controller but somehow it reflects only on the second request.
So here is what I do: first I initialize my console app as follows:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace DynamicControllerServer
{
internal class Program
{
static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
ApplicationPartManager partManager = builder.Services.AddMvc().PartManager;
// Store thePartManager in my Middleware to be able to add controlelr after initialization is done
MyMiddleware._partManager = partManager;
// Register controller change event
builder.Services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
builder.Services.AddSingleton(MyActionDescriptorChangeProvider.Instance);
var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
// Add Middleware which is responsible to cactn the request and dynamically add the missing controller
app.UseMiddleware<MyMiddleware>();
app.RunAsync();
Console.WriteLine("Server has been started successfully ...");
Console.ReadLine();
}
}
}
Then my middleware looks like this: it basically detects that there is the "dynamic" keyword in the url. If so, it will load the assembly containing the DynamicController:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using System;
using System.Reflection;
namespace DynamicControllerServer
{
public class MyMiddleware
{
public RequestDelegate _next { get; }
private string dllName = "DynamicController1.dll";
static public ApplicationPartManager _partManager = null;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Path.HasValue)
{
var queryParams = httpContext.Request.Path.Value;
if(httpContext.Request.Path.Value.Contains("api/dynamic"))
{
// Dynamically load assembly
Assembly assembly = assembly = Assembly.LoadFrom(#"C:\Temp\" + dllName);
// Add controller to the application
AssemblyPart _part = new AssemblyPart(assembly);
_partManager.ApplicationParts.Add(_part);
// Notify change
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
}
}
await _next(httpContext); // calling next middleware
}
}
}
The ActionDescriptorChange provider looks like this:
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Primitives;
namespace DynamicControllerServer
{
public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
}
Dynamic controller is in separate dll and is very simple:
using Microsoft.AspNetCore.Mvc;
namespace DotNotSelfHostedOwin
{
[Route("api/[controller]")]
[ApiController]
public class DynamicController : ControllerBase
{
public string[] Get()
{
return new string[] { "dynamic1", "dynamic1", DateTime.Now.ToString() };
}
}
}
Here are the packages used in that project:
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
This works "almost" fine ... when first request is made to:
https://localhost:5001/api/dynamic
then it goes in the middleware and load the assembly, but returns a 404 error.
Then second request will actually work as expected:
Second request returns the expected result:
I must doing it wrong and probably my middleware is executed too late in the flow to reflect the dynamic controller right away.
Question is: what should be the proper way to achieve this?
Second question I have is say now the external dll holding our dynamic controller is updated.
How can I reload that controller to get the new definition?
Any help would be appreciated
Thanks in advance
Nick
Here is the answer to my own question in case it can help somebody out there.
It seems building and loading the controller from the middleware will always end up with failure on the first call.
This makes sense since we are already in the http pipeline.
I end up doing same thing from outside the middleware.
Basically my application detect a change in the controller assembly, unload the original assembly and load the new one.
You cannot use the Default context since it will not allow reloading different dll for same assembly:
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); // Produce an exception on updates
To be able to reload new dll for same assembly, I’m loading each controller in its own assembly context. To do that you need to create your own class deriving from AssemblyLoadContext and managing assembly load:
public class MyOwnContext: AssemblyLoadContext
{
// You can find lots of example in the net
}
When you want to unload the assembly, you just unload the context:
MyOwnContextObj.Unload();
Now to add or remove the controller on the fly, you need to keep reference of the PartManager and the ApplicationPart.
To add controller
ApplicationPart part = new AssemblyPart(assembly);
_PartManager.ApplicationParts.Add(part);
To remove:
_PartManager.ApplicationParts.Remove(part);
On course once done, still use following piece of code to acknowledge the change:
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
That allow updating controller on the fly with no interruption of service.
Hope this helps people out there.
I have done a similar solution (used for managing a web app plugins) with some differences that may help you:
List all the external assemblies in a config file or appsettings.json so all the dll names and/or addresses are known at startup
Instead of registering controllers when they are called, register them at program.cs/start up :
//Foreah dllName from settings file
var assembly = Assembly.LoadFrom(#"Base address" + dllNameLoadedFromSettings);
var part = new AssemblyPart(assembly);
services.AddControllersWithViews()
.ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part));
// Any other configuration based on the usage you want
Second: I usually keep plugin dlls in the bin folder so when using IIS as soon as a dll file in bin is changed the upper-level app is automatically reset. So your second question would be solved too.

Deriving device connection string from the environment

IoT modules can be created from the environment using :
ModuleClient.CreateFromEnvironmentAsync(settings)
However, there does not seem to be an equivalent method for devices. For now, I am setting the device connection string in the program to test it out, but is there a better way to read teh connection string from iotedge/config.yaml for all the edge devices deployed out there?
Methods to do so for .NET and python would be appreciated.
You can use a yaml parse library to deserialize the document, such as YamlDotNet. In fact, you can refer to YamlDocument in iot edge. But in the class, it does not provide a method to get the key value. Please refer to following code.
public class YamlDocument
{
readonly Dictionary<object, object> root;
public YamlDocument(string input)
{
var reader = new StringReader(input);
var deserializer = new Deserializer();
this.root = (Dictionary<object, object>)deserializer.Deserialize(reader);
}
public object GetKeyValue(string key)
{
if(this.root.ContainsKey(key))
{
return this.root[key];
}
foreach(var item in this.root)
{
var subItem = item.Value as Dictionary<object, object>;
if(subItem != null && subItem.ContainsKey(key))
{
return subItem[key];
}
}
return null;
}
}
And then you can get the device connection string from the config.yaml. If you use python, you can import yaml library to analysis the file.
StreamReader sr = new StreamReader(#"C:\ProgramData\iotedge\config.yaml");
var yamlString = sr.ReadToEnd();
var yamlDoc = new YamlDocument(yamlString);
var connectionString = yamlDoc.GetKeyValue("device_connection_string");
Console.WriteLine("{0}", connectionString);
To get the config file from the host, add the following to the docker deployment file. Note that the source file is config1.yaml which is the same as config.yaml except that it has read permissions for everyone not just root.
"createOptions": "{\"HostConfig\":{\"Binds\":[\"/etc/iotedge/config1.yaml:/app/copiedConfig.yaml\"]}}"
With the above line in place, the copiedConfig.yaml file can be used in the container, along with #Michael Xu's parsing code to derive teh connection string.
Long term, one may want to use the device provisioning service anyway but hope this helps for folks using device conenction strings for whatever reason..

Ektron CMS - new extension for OnAfterAdd not working

I followed the direction here for adding a new extension so that I can trigger an event whenever a new image is uploaded to Ektron. I created this new file in the App_Code folder of my project:
using System;
using System.Collections.Generic;
using System.Text;
using Ektron.Cms;
using Ektron.Cms.Common;
using Ektron.Cms.Extensibility;
using Ektron.Cms.Extensibility.Content;
namespace Cms.Extensions.Samples
{
public class UploadExtension : LibraryStrategy
{
public override void OnAfterAdd(LibraryData taxonomyData, CmsEventArgs eventArgs)
{
string[] lines = { "Written on Ektron upload event!" };
System.IO.File.WriteAllLines(#"C:\Users\Public\TestFolder\WORKING.txt", lines);
var x = taxonomyData;
}
public override void OnAfterUpdate(LibraryData taxonomyData, CmsEventArgs eventArgs)
{
var x = taxonomyData;
}
public override void OnBeforeDelete(long id, CmsEventArgs eventArgs)
{
var x = id;
}
}
}
I just put in one test line for each method so that I could add a breakpoint to see if it's getting hit. I registered the new extension in objectfactory:
<objectFactory>
<objectStrategies>
<add name="Library">
<strategies>
<add name="EktronUploadExtension" type="Cms.Extensions.Samples.UploadExtension"/>
<add name="GoogleGeoCoder" type="Cms.Extensions.GoogleGeoCoder.LibraryStrategy, Cms.Extensions.GoogleGeoCoder"/>
</strategies>
</add>
</objectStrategies>
</objectFactory>
It looks like I set everything up correctly, but I attached to process and opened up my Ektron work area and uploaded a new image to the library, but none of my breakpoints (specifically the breakpoint in OnAfterAdd) got hit. I'm not sure how to debug or figure out what's wrong with my extension.
EDIT: I fixed the objectfactory.config file, but it's still not working. The breakpoints in UploadExtension.cs aren't working, and the test file that I put in the function isn't getting written when I add new library item in Ektron.
Your objectfactory.config file is incorrect. You have created a LibraryStrategy but placed it into the Content Strategy section of the objectfactory.config.
You should add a section called "Library" to the config file like so:
<add name="Library">
<strategies>
<add name="MyFirstExample"
type="Cms.Extensions.Samples.UploadExtension"/>
</strategies>
</add>

ASP MVC setting defining images folder

I am a newbie in asp mvc, and would like to define links in views to image/content folder in a way so I don't have to change each link if a image folder changes.
Is is possible using ActionLink and routing, bundling or there is a better way to achieve this.
I could not find a good example anywhere so I did not try anything any coding far.
I am thinking of storing a fixed path somewhere, but is that really a mvc type solution?
There are a number of ways you could do this. Here's one approach to extend the Url.Content() method.
1. Create an extension method
We'll called it Virtual().
namespace TestApp.Extensions
{
public static class UrlHelperExtensions
{
private const string _settingPattern = "path:{0}";
private const string _regexPattern = #"\{\w+\}";
public static string Virtual(this UrlHelper helper, string url)
{
Regex r = new Regex(_regexPattern);
var matches = r.Matches(url);
if (matches.Count == 0) return url;
var sb = new StringBuilder(url);
var keys = WebConfigurationManager.AppSettings.AllKeys;
foreach (var match in matches)
{
string key = match.ToString().TrimStart('{').TrimEnd('}');
string pattern = string.Format(_settingPattern, key);
foreach (var k in keys)
{
if (k == pattern)
{
sb.Replace(match.ToString(), WebConfigurationManager.AppSettings.Get(k));
}
}
}
return helper.Content(sb.ToString());
}
}
}
2. Add settings to the main Web.config
Freely add any paths you want.
<add key="path:images" value="~/Content/images" />
<add key="path:scripts" value="~/scripts" />
3. Add the namespace to the Web.config of your views directory
<namespaces>
<add namespace="TestApp.Extensions"/>
</namespaces>
4. Use the new method
#Url.Virtual("{images}/mypic.png")
Output:
/Content/images/mypic.png
You can now use Virtual() where you would Content().
This solution is arguably excessive, but it is comprehensive.

Replace Sharp Architecture's NHibernate.config with a Fluent Configuration

By default, the solution generated from Sharp Architecture's templify package configures NHibernate using an NHibernate.config file in the {SolutionName}.Web project. I would like to replace it with a fluent configuration of my own and still have the rest of Sharp Architecture work correctly.
Any help will be much appreciated. :)
Solution: Here's how I got it to work:
IPersistenceConfigurer configurer = OracleClientConfiguration.Oracle10
.AdoNetBatchSize(500)
.ShowSql()
.ConnectionString(c => c.FromConnectionStringWithKey("NHibernate.Localhost"))
.DefaultSchema("MySchema")
.ProxyFactoryFactory("NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle")
.UseReflectionOptimizer();
NHibernateSession.Init(
webSessionStorage,
new string[] { Server.MapPath("~/bin/MyProject.Data.dll") },
new AutoPersistenceModelGenerator().Generate(),
null,
null,
null,
configurer);
iirc the NhibernateSession class that is used to configure nhibernate has a bunch of overloads one of them giving you the ability to configure it via code.
Very old post. I'll leave it here in case someone else is interested. On SharpArch 1.9.6.0 you can add two methods to NHibernateSession.cs. This will let you pass-in a FluentConfiguration object.
public static FluentConfiguration Init(ISessionStorage storage, FluentConfiguration fluentConfiguration)
{
InitStorage(storage);
try
{
return AddConfiguration(DefaultFactoryKey, fluentConfiguration);
}
catch
{
// If this NHibernate config throws an exception, null the Storage reference so
// the config can be corrected without having to restart the web application.
Storage = null;
throw;
}
}
private static FluentConfiguration AddConfiguration(string defaultFactoryKey, FluentConfiguration fluentConfiguration)
{
var sessionFactory = fluentConfiguration.BuildSessionFactory();
Check.Require(!sessionFactories.ContainsKey(defaultFactoryKey),
"A session factory has already been configured with the key of " + defaultFactoryKey);
sessionFactories.Add(defaultFactoryKey, sessionFactory);
return fluentConfiguration;
}

Resources