|
After laborious effort I was able to understand what was happening. Generally, I would consider the behavior in the OP as a bug with the .NET code since it can't read in what it writes out; however, the code seems structured and designed to produce this behavior; therefore, the behavior doesn't appear to be a mistake, just a sore and undocumented design with unintuitive behavior. Since this is such odd, undocumented and unintuitive behavior I felt compelled to share my findings with posterity.
Here is what I found:
Public APIs:
System.Configuration.ConfigurationSectionGroup
This is the configuration construct used to group sections and other groups, essentially a container. It can be subclassed but itself derives from nothing and offers little functionality so it offers little opportunity for customization.
System.Configuration.Configuration
Similar to ConfigurationSectionGroup in that it essentially acts as a container for sections and groups and derives from nothing; however, it is sealed and cannot be subclassed. It provides contextual information, some fundamental constructs for processing connection strings and application settings and the API for saving the configuration to file. It provides some flexibility for customization via delegate actions.
So, by and large, most of the processing of configuration files is behind the scenes and internal to the .NET framework. Therefore, to study these mechanisms required looking into the .NET code base and decompiling the necessary code in the VS debugger.
System.Configuration.MgmtConfigurationRecord
System.Configuration.BaseConfigurationRecord
These classes are internal to the .NET framework and provide much of the guts of configuration management. Simply, each configuration [file] is associated with one configuration record. The configuration record provides bookkeeping of what is in the configuration file and how the configuration information is written.
SaveAs(string filename, ConfigurationSaveMode saveMode, bool forceUpdateAll)
This method is called whenever a user wants to save the configuration file. It manages the save process, essentially providing a wrapper around save functionality and providing error handling. It calls CopyConfig(...) whose job it is to copy over all original configuration file content (comments and all) and integrate the new changes. It calls the following methods:
CopyConfigDeclarationsRecursive(...)
This method copies all content from the declarations section
<configuration>
<configSections>
</configSections>
</configuration>
of the original configuration file. It then calls
WriteUnwrittenConfigDeclarations(...)
which writes any new declarations.
Herein lies the rub!
NEW SECTIONS AND SECTION GROUPS ARE WRITTEN AFTER EXISTING SECTIONS AND GROUPS.
Intuitive, yes? Wrong!
Consider the configuration file XML from the OP:
<configuration>
<configSections>
<sectionGroup name="ServiceConfiguration" type="My.ServiceConfiguration" >
<section name="CustomSection" type="My.CustomSection" />
<sectionGroup name="ServiceStepGroups" type="My.StepGroups" >
<sectionGroup name="MyGroup1" type="My.StepGroup" />
</sectionGroup>
</sectionGroup>
<sectionGroup name="ServiceConfiguration" >
<sectionGroup name="ServiceStepGroups" >
<sectionGroup name="MyGroup2" type="My.StepGroup" />
</sectionGroup>
</sectionGroup>
</configSections>
<ServiceConfiguration>
<ServiceStepGroups>
<CustomSection />
<MyGroup1 />
<MyGroup2 />
</ServiceStepGroups>
</ServiceConfiguration>
</configuration>
We have the main custom section group ServiceConfiguration which holds all business-type configuration information which is unique or specific to the application (as opposed to standard types of configuration constructs such application settings, runtime, service model, etc.). Inside this main group we have the ServiceStepGroups group which in turn declares various groups of steps for the application to perform (MyGroup1 and MyGroup2 ).
However, there are two (2) ServiceConfiguration declarations, a clear violation of the configuration specification which requires uniquely named groups. In the original configuration file we only had MyGroup1 defined where it and its containing groups were declared with the type attribute, necessary for declaring the .NET group type when loading a configuration file. But, we added MyGroup2 after loading the configuration file so that we could re-save the configuration file with the new group.
Here begins the unintuitive behavior .
Because CopyConfigDeclarationsRecursive(...) is called first it writes a complete ServiceConfiguration declaration as copied from the original file. When WriteUnwrittenConfigDeclarations(...) is called it has no choice but to write a second ServiceConfiguration declaration in order to maintain the declaration hierarchy; however, it does so without type attributes, presumably since the .NET type is not required for writing but only when reading. If a declaration (such as ServiceStepGroups ) was not present in the original declaration then the type is written; if a declaration was present a type is not written.
So, this behavior would seem to be a design flaw in SaveAs(...) . However, in the interest of open-mindedness I will hold back eternal judgment since there may be competing objectives or other behavioral scenarios of which I am not aware which drove Microsoft's design. Nonetheless, I think we can all agree that better documentation would have been helpful in understanding the behavior and function of programmatically modifying and saving configuration files. If there is better documentation I could not find it.
The Solution:
After all was discovered and understood I was able to produce a proper configuration file by implementing a post-processing step . It was a workaround: manually modify the XML of the new configuration file (I used the System.Xml libraries) to move new group declarations to their proper place within the original ServiceStepGroups declaration and remove the duplicate hierarchical constructs.
I could have done this in the beginning but I had assumed I didn't need to (based on expectations of intuitive functionality and lack of documentation) and that the problem was with me and my code utilizing the .NET configuration mechanisms. While vindicated technically it was a good (but agonizing ) lesson in the underlying mechanisms of .NET configuration processing and its limitations.
So ending on a positive note maybe this technical dive and case study can provide some documentation or reference to others experiencing a similar issue in the future. I am reminded of a quote from John Adams:
> Posterity! you will never know how much it cost the present generation to preserve your freedom.
> I hope you will make a good use of it. If you do not, I shall repent in Heaven that I ever
> took half the pains to preserve it
Posterity, I hope you make good use of this information.
modified 28-Oct-21 16:29pm.
|
|
|
|
|
This is definitely worth writing up as a Tip/Trick!
That will make the results of your spelunking more permanently visible and searchable.
Forum messages like this are, by their very nature, transient.
Cheers,
Peter
Software rusts. Simon Stephenson, ca 1994. So does this signature. me, 2012
|
|
|
|
|
Thanks for the feedback. How do I do this? Or is that a forum SysAdmin prerogative?
|
|
|
|
|
Start here[^]
If you run into any problems, post a message in that forum and Sean Ewington will pop up to help you.
Software rusts. Simon Stephenson, ca 1994. So does this signature. me, 2012
|
|
|
|
|
Member 12660776 wrote: How do I do this?
You could start here: Submit a new Article[^]
And no, you do not need admin prerogatives to submit an article/tip/trick/reference.
Yours would make a nice tip/trick, indeed. The process of finding the cause is at least as important as the actual resolution, imho.
"Five fruits and vegetables a day? What a joke!
Personally, after the third watermelon, I'm full."
|
|
|
|
|
I am working on Webhooks Concept. Since Webhooks act like an trigger. It will be firing an event like Update or Insert takes place. Its a MVC application and I have two Controller Home and Webhook Controller. I am passing argument through Query String on Home controller. I have tried Session,TempData, Cookies for passing variable from home to Webhooks HttpContext.Session.SetString("UserName", Request.Query["UserName"].ToString());
and getting session value on webhook controller
HttpContext.Session.GetString("UserName");
Also tried the same for TempData
Issue is On webbooks Controller its coming null.
I need to store parameter (User Name ,password) and reused the same for database related operation
Kindly guide
|
|
|
|
|
Session and TempData both rely on cookies to identify the requesting browser. They have a limited life-span, so data you store in them won't hang around forever.
If the data isn't available in the web-hook request, that means either the session has timed out between the initial request and the hook request; or the hook request was made from a different process than the initial request, or using a tool which doesn't store the cookies you set.
You will need to find a different solution. Since you haven't provided any details of what you're actually trying to do, we can't offer any suggestions.
NB: Storing credentials in memory is not a good idea.
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
This is my home controller....
Where I am passing value in query string.......
using IdentityModel.Client;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json;
using NLog;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using Newtonsoft;
using System.Text.RegularExpressions;
using System.Net.Http.Headers;
using System.Diagnostics;
using Newtonsoft.Json.Linq;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace MYApp.Controllers
{
public class HomeController : Controller
{
Logger logger = LogManager.GetCurrentClassLogger();
private string clientId = "";
private string clientSecret = "";
private ModelStateDictionary ValidationMessages { get; set; }
public class UserInfo
{
public string Addresses { get; set; }
public string OriginatorNo { get; set; }
public string OriginatorName { get; set; }
public string ContactName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
public IActionResult Index()
{
try
{
string strUrl = "https://url";
if (Request.Query["UserName"].ToString() != string.Empty && Request.Query["Password"] != string.Empty)
{
HttpContext.Session.SetString("UserName", Request.Query["UserName"].ToString());
HttpContext.Session.SetString("Password", Request.Query["strPassword"].ToString());
}
}
catch (Exception ex)
{
}
}
public class ContactPerson
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string IncludeInEmails { get; set; }
}
public IActionResult DataBase_Import(string Json,string page)
{
string DecodeJson = "";
string strNUrl = "";
string pageName = page;
string strToken = "";
string strRefreshToken = "";
string strAuthToken = "";
if (HttpContext.Session.GetString("UserName") != null)
{
if (HttpContext.Session.GetString("UserName") != "")
{
UserName = HttpContext.Session.GetString("UserName");
}
}
if (HttpContext.Session.GetString("Password") != null)
{
if (HttpContext.Session.GetString("Password") != "")
{
Password = HttpContext.Session.GetString("Password");
}
}
Data Logic here......
}
}
This is My Webbooks Controller
I am trying to get username and password on this controller so that whenever the event is fired I can update my database based on username and password.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.ComponentModel;
using Newtonsoft.Json;
using XeroApp.dto;
using Newtonsoft.Json.Linq;
using System.Data;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using XeroApp.Models;
namespace XeroApp.Controllers
{
[Route("/webhooks")]
public class WebhooksController : Controller
{
public string strEventCategory = "";
public string strresourceId = "";
public string eventDateUt = "";
public string eventType = "";
public string strTenanId = "";
public string agrno = "", EmailId = "", Password = "", RedirectUrl = "";
private readonly IOptions<webhooksettings> webhookSettings;
private readonly ConcurrentQueue<payload> payloadQueue = new ConcurrentQueue<payload>();
private BackgroundWorker _ProcessPayloadWorker = new BackgroundWorker();
public WebhooksController(IOptions<webhooksettings> webhookSettings)
{
this.webhookSettings = webhookSettings;
// Configure background worker
_ProcessPayloadWorker.WorkerSupportsCancellation = true;
_ProcessPayloadWorker.DoWork += ProcessPayloadWorker_DoWork;
}
// Method called when webhook notification sent
[HttpPost]
public IActionResult Index()
{
string val = "";
string strToken = "";
string strRefreshToken = "";
string strAuthToken = "";
if (HttpContext.Session.GetString("UserName") != null)
{
if (HttpContext.Session.GetString("UserName") != "")
{
UserName = HttpContext.Session.GetString("UserName");
}
}
if (HttpContext.Session.GetString("Password") != null)
{
if (HttpContext.Session.GetString("Password") != "")
{
Password = HttpContext.Session.GetString("Password");
}
}
var payloadString = GetRequestBody().Result.ToString();
var signature = Request.Headers[webhookSettings.Value.XeroSignature].FirstOrDefault();
string strPdfSrtring = "";
if (!VerifySignature(payloadString, signature))
{
// Webhook signature invalid, reject payload
return Unauthorized();
}
else
{
// lstEvents = payloadString.ToList<xerowebhookevent>;
// List<xerowebhook> selectedCollection = payloadString.ToList<xerowebhook>;
}
JsonConvert.DeserializeObject<payload>(payloadString);
// Valid signature, enqueue payload to queue and start asynchronous processing of payload
payloadQueue.Enqueue(JsonConvert.DeserializeObject<payload>(payloadString));
foreach(var item in payloadQueue)
{
foreach(var eve in item.Events)
{
strEventCategory = eve.EventCategory.ToString();
eventDateUt = eve.EventDateUtc.ToString();
eventType = eve.EventType.ToString();
strresourceId = eve.ResourceId.ToString();
strTenanId = eve.TenantId.ToString();
if (strresourceId != ""&& UserName!="")
{
if (eve.EventCategory.ToString() == "INVOICE")
{
// HelpClass.GetInvoiceById(strresourceId);
}
if (eve.EventCategory.ToString() == "CONTACT")
{
HelpClass.GetContactById(UserName,Password);
}
HelpClass.WebHookEventInsert(UserName,Password);
}
}
_ProcessPayloadWorker.RunWorkerAsync();
return Ok();
}
// Validate webhook signature, signature must match hash of json payload using webhook key as the hash key
private bool VerifySignature(string payload, string signature)
{
var encoding = new System.Text.ASCIIEncoding();
byte[] keyByte = encoding.GetBytes(webhookSettings.Value.WebhookKey);
byte[] payloadByte = encoding.GetBytes(payload);
using (var hmac = new HMACSHA256(keyByte))
{
byte[] hashMessage = hmac.ComputeHash(payloadByte);
var hashMsg = Convert.ToBase64String(hashMessage);
return Convert.ToBase64String(hashMessage) == signature ? true : false;
}
}
// Invokes background worker to process payload
private void ProcessPayloadWorker_DoWork(object sender, DoWorkEventArgs e)
{
ProcessPayloadQueue();
}
// Use method to process payloads
private void ProcessPayloadQueue()
{
while (payloadQueue.Count > 0)
{
payloadQueue.TryDequeue(out Payload payload);
foreach (PayloadEvent payloadEvent in payload.Events)
{
// Process payloads here
Debug.WriteLine("\nEvent Type: " + payloadEvent.EventType.ToString());
Debug.WriteLine("Event Category: " + payloadEvent.EventCategory.ToString() + "\n");
}
}
}
}
}
Kindly provide the solution through which i can get the required values whenever the webbooks event is fired
modified 14-Sep-21 10:27am.
|
|
|
|
|
I'm now designing a new application that will run only on Windows. No other platforms.
Is there any reason to develop it in .NET Core as opposed to Framework?
The difficult we do right away...
...the impossible takes slightly longer.
|
|
|
|
|
AFAIK, .NET Framework 4.8 is the last of the Windows specific framework. Everything goes .NET (Core) from now on, so it's only a matter of time before your app needs to be converted to a .NET app.
|
|
|
|
|
Technically, the choice is between .NET Framework 4.8 and .NET 5 now.
Announcing .NET 5.0 | .NET Blog[^]
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
[HttpPatch("{id:int}")]
public async Task<IActionResult>UpdatePartialAsync(int id, [FromBody] JsonPatchDocument<TEntity> patchEntity)
{
var entity = await _service.ReadAsync(id, false);
if (entity == null)
return NotFound();
try
{
patchEntity.ApplyTo(entity, ModelState);
}
catch(Exception ex)
{
return Ok(ex.Message);
}
entity = await _service.UpdateAsync(id, entity);
return Ok(entity);
}
When accessing this in PostMan, the return is an error, patchEntity has one Operation with all values being null.
Any ideas?
|
|
|
|
|
thewizardoftn wrote: the return is an error
Are we supposed to guess what the error is?
thewizardoftn wrote:
return Ok(ex.Message); Returning an "OK" response for an exception seems like a particularly bad idea.
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
You didn't check if patchEntity was null before dereferencing it.
"Fairy tales do not tell children the dragons exist. Children already know that dragons exist. Fairy tales tell children the dragons can be killed."
- G.K. Chesterton
|
|
|
|
|
i got error when i want to add sql server in visual studio 2019
Unable to add data connection. Could not find any resources appropriate for the specified culture or the neutral culture. Make sure -Microsoft.VisualStudio.Data.Providers.SqlSetver.SqlViewSupp ort.xmr was correctly embedded or linked into assembly Microsoft.VisualStudio.Data.Providers.SqlServer at compile time, or that all the satellite assemblies required are loadable and fully signed.
|
|
|
|
|
This is exactly the same question as the one below. And if both of those accounts belong to you, you should delete one of them.
|
|
|
|
|
In Visual studio 2019, when opening the database, the program displays the message:
Could not find any resources appropriate for the specified culture or the neutral culture.
Make sure "Microsoft.VisualStudio.Data.Providers.SqlServer.SqlViewSupport.xml" was correctly embedded or linked into assembly
"Microsoft.VisualStudio.Data.Providers.SqlServer" at compile time,
or that all the satellite assemblies required are loadable and fully signed.
Can anyone advise me how to solve this problem?
Well thank you
|
|
|
|
|
|
Hi all, how can I add the x clear button to textbox controls on a web page ? I've googled and only see references to Telerik - is this doable without third party controls ?
"I didn't mention the bats - he'd see them soon enough" - Hunter S Thompson - RIP
|
|
|
|
|
The browser will do it if you use type="search" instead of type ="text". Otherwise, I believe you'll have to write your own css and JS to do it.
There's probably examples online if you want to do it custom.
|
|
|
|
|
Hi, I tried type = search but no cross - not a big deal as it's only a personal project
"I didn't mention the bats - he'd see them soon enough" - Hunter S Thompson - RIP
|
|
|
|
|
pkfox wrote: I tried type = search What browser? You do have to start typing for it to show up.
See Tryit Editor v3.6[^]
|
|
|
|
|
Hi and thanks for replying, I use Firefox, tried the link you posted and still no x
Tried it with Edge and it worked
"I didn't mention the bats - he'd see them soon enough" - Hunter S Thompson - RIP
|
|
|
|
|
Yep, you're right. I thought all browsers had started doing that now but I guess not.
|
|
|
|
|
Never mind - as I said it's only a personal project - thanks again
"I didn't mention the bats - he'd see them soon enough" - Hunter S Thompson - RIP
|
|
|
|