for the sake of having kind of the same setup in Java and C# (i know, terrible reason), I got this setup running in Java:
log4j:
<appender name="ELASTIC" class="ch.qos.logback.core.FileAppender">
...
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="FileBeatLayout" />
</encoder>
</appender>
Some information logging into MDC
MDC.Set("testIssue", testIssue.Replace("#", "-"));
MDC.Set("testType", testType);
MDC.Set("method", method.Name.ToLower());
MDC.Set("elapsedTime", elapsedTimeInNs.ToString());
MDC.Set("elapsedTimeUnit", "ns");
MDC.Set("platform", ".NET");
MDC.Set("platformVersion", Environment.Version.ToString());
MDC.Set("operatingSystem", $"{GetBaseOs()} {Environment.OSVersion.Version.Major}");
MDC.Set("jenkinsBuild", GetJenkinsBuildTagIfSet());
foreach (var version in testEnvironment.GetDependencyTreeDict())
{
MDC.Set(version.Key, version.Value);
}
_logger.Info("[{0}] *{1}* {2}: {3} ns", testIssue, testType, method.Name, elapsedTimeInNs);
log4net.ThreadContext.Properties["TIMING"] = "false";
MDC.Clear();
Finaly a custom Layout class writing the content of the MDC to the file as one json string per logging event. (Java)
public class FileBeatLayout extends LayoutBase<ILoggingEvent> {
#Override
public String doLayout(ILoggingEvent iLoggingEvent) {
StringBuilder stringBuilder = new StringBuilder();
Map<String, String> map = iLoggingEvent.getMDCPropertyMap();
JSONObject jsonObject = new JSONObject(map);
stringBuilder.append(jsonObject);
return stringBuilder.toString().replaceAll("[\\t \\n]", "") + "\n";
}
}
how do I do iterate over all values in MDC in C#, as in slf4net, this functionality is internal? I got this far (and this works insofar that it will write out that string for each event)
public class FileBeatLayout : LayoutSkeleton
{
public override void ActivateOptions()
{
}
public override void Format(TextWriter writer, LoggingEvent loggingEvent)
{
writer.WriteLine("The JSON object containing all MDC values");
}
}
Found out myself after rubber ducking stackoverflow:
ThreadContext.Properties.GetKeys().ForEach(x => jsonObject.Add(x, ThreadContext.Properties[x]));
This seems to be the way to access whatever you write to MDC.
Related
I'm working on a Window Forms application in Visual Studio, and I'm using a custom settings object to keep track of some application settings.
The user can change these settings through the PropertyGrid widget.
This works great for string and integer values, but now I also want to add a List<string> variable, so the user can enter a list of keywords.
I've added the List<string> variable to the settings object and I've added a TypeConverter to show it as a comma separated string representation in the PropertyGrid. Without the TypeConverter the value would display as just (Collection). It is displayed correctly and I can edit it, see screenshot below
this._MyProps = new PropsClass();
this._MyProps.ReadFromIniFile("mysettings.ini");
propertyGrid1.SelectedObject = this._MyProps;
Now I also want to write and read these setting to a settings.ini file, so I've added SaveToIniFile and ReadFromIniFile methods to the object. This works for string and integer values, except the List<string> is not saved and loaded to and from the .ini file correctly. When I call SaveToIniFile the content mysettings.ini is for example this, still using the "(Collection)" representation and not the values entered by the user:
[DataConvert]
KeyWordNull=NaN
ReplaceItemsList=(Collection)
YearMaximum=2030
So my question is, how can I save/load a List<string> setting to an ini file while also allowing the user to edit it in a PropertyGrid?
I know it'd have to convert from a string to a List somehow, maybe using quotes around the string to inclkude the line breaks, or maybe just comma-separated back to a list of values? But anyway I thought that is what the TypeConverter was for. So why is it showing correctly in he PropertyGrid but not in the ini file? See code below
The custom settings properties object:
// MyProps.cs
public class PropsClass
{
[Description("Maximum year value."), Category("DataConvert"), DefaultValue(2050)]
public int YearMaximum { get; set; }
[Description("Null keyword, for example NaN or NULL, case sensitive."), Category("DataConvert"), DefaultValue("NULL")]
public string KeyWordNull { get; set; }
private List<string> _replaceItems = new List<string>();
[Description("List of items to replace."), Category("DataConvert"), DefaultValue("enter keywords here")]
[Editor("System.Windows.Forms.Design.StringCollectionEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[TypeConverter(typeof(StringListConverter))]
public List<string> ReplaceItemsList
{
get
{
return _replaceItems;
}
set
{
_replaceItems = value;
}
}
and in the same PropsClass class, the write and read methods to save/load from a settings.ini file
[DllImport("kernel32.dll")]
public static extern int GetPrivateProfileSection(string lpAppName, byte[] lpszReturnBuffer, int nSize, string lpFileName);
public void SaveToIniFile(string filename)
{
// write to ini file
using (var fp = new StreamWriter(filename, false, Encoding.UTF8))
{
// for each different section
foreach (var section in GetType()
.GetProperties()
.GroupBy(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false)
.FirstOrDefault())?.Category ?? "General"))
{
fp.WriteLine(Environment.NewLine + "[{0}]", section.Key);
foreach (var propertyInfo in section.OrderBy(x => x.Name))
{
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
fp.WriteLine("{0}={1}", propertyInfo.Name, converter.ConvertToInvariantString(propertyInfo.GetValue(this, null)));
}
}
}
}
public void ReadFromIniFile(string filename)
{
// Load all sections from file
var loaded = GetType().GetProperties()
.Select(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General")
.Distinct()
.ToDictionary(section => section, section => GetKeys(filename, section));
//var loaded = GetKeys(filename, "General");
foreach (var propertyInfo in GetType().GetProperties())
{
var category = ((CategoryAttribute)propertyInfo.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General";
var name = propertyInfo.Name;
if (loaded.ContainsKey(category) && loaded[category].ContainsKey(name) && !string.IsNullOrEmpty(loaded[category][name]))
{
var rawString = loaded[category][name];
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
if (converter.IsValid(rawString))
{
propertyInfo.SetValue(this, converter.ConvertFromString(rawString), null);
}
}
}
}
// helper function
private Dictionary<string, string> GetKeys(string iniFile, string category)
{
var buffer = new byte[8 * 1024];
GetPrivateProfileSection(category, buffer, buffer.Length, iniFile);
var tmp = Encoding.UTF8.GetString(buffer).Trim('\0').Split('\0');
return tmp.Select(x => x.Split(new[] { '=' }, 2))
.Where(x => x.Length == 2)
.ToDictionary(x => x[0], x => x[1]);
}
}
and the TypeConverter class for the ReplaceItemsList property
public class StringListConverter : TypeConverter
{
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (value is List<string>)
{
return string.Join(",", ((List<string>)value).Select(x => x));
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
The reason your type converter is not used is because of this line:
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
You are getting the TypeConverter that is defined on the type of the property. So for ReplaceItemsList that would be the TypeConverter for List<T>. You need to get the TypeConverter for the property since that is where you added the TypeConverter attribute. So either you do something like you did for the category attribute in the read method where you use the PropertyInfo's GetCustomAttributes or you do what the PropertyGrid does which is use the PropertyDescriptors to get to the properties and their state. The latter would be better since if the object implemented ICustomTypeDescriptor or some other type augmentation like TypeDescriptionProvider then you would get that automatically.
So something like the following for the Save using PropertyDescriptors would be:
public void SaveToIniFile(string filename)
{
// write to ini file
using (var fp = new StreamWriter(filename, false, Encoding.UTF8))
{
// for each different section
foreach (var section in TypeDescriptor.GetProperties(this)
.Cast<PropertyDescriptor>()
.GroupBy(x => x.Attributes.Cast<Attribute>().OfType<CategoryAttribute>()
.FirstOrDefault()?.Category ?? "General"))
{
fp.WriteLine(Environment.NewLine + "[{0}]", section.Key);
foreach (var propertyInfo in section.OrderBy(x => x.Name))
{
var converter = propertyInfo.Converter;
fp.WriteLine("{0}={1}", propertyInfo.Name, converter.ConvertToInvariantString(propertyInfo.GetValue(this)));
}
}
}
}
If you are going to use a custom TypeConverter, you'll have to register it as a provider to TypeDescriptionProvider:
TypeDescriptor.AddProvider(new CustumTypeDescriptorProvider(), typeof(List<string>));
And in your implementation you could just do this in the constructor of PropsClass (instead of using the attribute). I created some custom code below that would do the split.
public class PropsClass
{
[DllImport("kernel32.dll")]
public static extern int GetPrivateProfileSection(string lpAppName, byte[] lpszReturnBuffer, int nSize, string lpFileName);
[Description("Maximum year value."), Category("DataConvert"), DefaultValue(2050)]
public int YearMaximum { get; set; }
[Description("Null keyword, for example NaN or NULL, case sensitive."), Category("DataConvert"), DefaultValue("NULL")]
public string KeyWordNull { get; set; }
private List<string> _replaceItems = new List<string>();
[Description("List of items to replace."), Category("DataConvert"), DefaultValue("enter keywords here")]
[Editor("System.Windows.Forms.Design.StringCollectionEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public List<string> ReplaceItemsList
{
get
{
return _replaceItems;
}
set
{
_replaceItems = value;
}
}
public PropsClass()
{
TypeDescriptor.AddProvider(new CustumTypeDescriptorProvider(), typeof(List<string>));
}
public void SaveToIniFile(string filename)
{
// write to ini file
using (var fp = new StreamWriter(filename, false, Encoding.UTF8))
{
// for each different section
foreach (var section in GetType()
.GetProperties()
.GroupBy(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false)
.FirstOrDefault())?.Category ?? "General"))
{
fp.WriteLine(Environment.NewLine + "[{0}]", section.Key);
foreach (var propertyInfo in section.OrderBy(x => x.Name))
{
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
fp.WriteLine("{0}={1}", propertyInfo.Name, converter.ConvertToInvariantString(propertyInfo.GetValue(this, null)));
}
}
}
}
public void ReadFromIniFile(string filename)
{
// Load all sections from file
var loaded = GetType().GetProperties()
.Select(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General")
.Distinct()
.ToDictionary(section => section, section => GetKeys(filename, section));
//var loaded = GetKeys(filename, "General");
foreach (var propertyInfo in GetType().GetProperties())
{
var category = ((CategoryAttribute)propertyInfo.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General";
var name = propertyInfo.Name;
if (loaded.ContainsKey(category) && loaded[category].ContainsKey(name) && !string.IsNullOrEmpty(loaded[category][name]))
{
var rawString = loaded[category][name];
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
if (converter.IsValid(rawString))
{
propertyInfo.SetValue(this, converter.ConvertFromString(rawString), null);
}
}
}
}
// helper function
private Dictionary<string, string> GetKeys(string iniFile, string category)
{
var buffer = new byte[8 * 1024];
GetPrivateProfileSection(category, buffer, buffer.Length, iniFile);
var tmp = Encoding.UTF8.GetString(buffer).Trim('\0').Split('\0');
return tmp.Select(x => x.Split(new[] { '=' }, 2))
.Where(x => x.Length == 2)
.ToDictionary(x => x[0], x => x[1]);
}
}
public class CustumTypeDescriptorProvider : TypeDescriptionProvider
{
public override ICustomTypeDescriptor GetTypeDescriptor(System.Type objectType, object instance)
{
if (objectType.Name == "List`1") return new StringListDescriptor();
return base.GetTypeDescriptor(objectType, instance);
}
}
public class StringListDescriptor : CustomTypeDescriptor
{
public override TypeConverter GetConverter()
{
return new StringListConverter();
}
}
public class StringListConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string s = value as string;
if (!string.IsNullOrEmpty(s))
{
return ((string)value).Split(',').ToList();
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (destinationType == typeof(string))
{
return string.Join(",", (List<string>)value);
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
*NOTE: In my testing, the method ConvertFrom is called twice, once from converter.IsValid and once from propertyInfo.SetValue.
*NOTE2: You are using streamwriter to update the ini file. Since you are using GetPrivateProfileSection, it seems you should be using WritePrivateProfileSection to update the ini file.
*NOTE3: please consider the original comments in question about whether you should be using this method to read/write to an ini file. These methods have been around a long time. (https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getprivateprofilesection)
*NOTE4: A lot of this answer came from: Convert string to Array of string using TypeConverter
Leaving apart the PropertyGrid part which is just a way to put the info in a cartesian way on a Windows Form, the tricky part as I understand it, is to force a ini file (which is typically a 1:1 correlation between strings and strings) to house a collection of right hand side (rhs) values. Obviously, they cannot reside of a left hand side (lhs) of the ini line. You could, but then likely the choice of a ini file to persist your data is wrong at the roots.
So, why not avoiding to reinvent the wheel and go straight to custom parsing? This way you can also choose the separator to use (you may like to mimic windows, or apache setting files, etc).
Look at this implementation of one old code of mine. Grossly, it is an ini file telling my app which web exchange to contact for crypto trading. Before you get into a big headache with names: Monday is the name of the library holding the common code, and Friday is an app using Monday for trading online with a specific algo. Nothing important for the case being.
The ini file looks like this:
[Friday]
; if true the permutations will be only defined starting from the currency provided with index 0 in [Friday-Currencies]
MaxChainTransactions = 7
; if UseLastTradeValues is true, it will override UseConservativeBidAskValues
UseLastTradeValues = true
; if true the matrix values are (Ask+Bid)/2. This flag overrides UseConservativeBidAskValues but gets overridden by UseLastTradeValues
UseMidPriceValues = false
; if true the most conservative value between bid and ask will be used to calculate gains. Used only if UseLastTradeValues is false
UseConservativeBidAskValues = true
; if true, the solver will also solve with a matrix that will contain the best trade values from the available exchanges in parallel to other exchanges individually
MergeExchanges = false
; If true, Friday will issue one solution (if any) for each exchange, comprising one for the merged market if calculated.
DeliverOneSolutionPerExchange = true
; use values from the provided exchanges only
FridayExchanges = Bittrex| Binance | Kraken
; the minimum allowed gain remaining at the closure of the transaction chain to approve the full chain for ordering
MimimumAllowedGain = 1.05
; this is the target gain of a transaction. it is not used in case AllowPriceCorrectionBelowTargetGain is false
TargetGain = 1.09
; if true the prices in the transaction will be modified (increased towards the aggressive trade) of a factor equal to MaxPriceCorrectionFactor
AllowPriceCorrectionBelowTargetGain = true
; the frequency of each solution cycle (provided that the previous cycle has already completed and new data is arrived)
SolveEveryNSeconds = 15
Look at the line:
FridayExchanges = Bittrex| Binance | Kraken
it receives more than one exchange as a string separated by a pipe (|). You may choose other separators.
I suppose you parse the ini file with the support of some third party code. I used ini-parser (https://github.com/rickyah/ini-parser) that helped me a lot.
Parsing is done this way:
private static (int, string[]) ReadFridayExchanges()
{
string? configEntry = MondayConfiguration.MondaySettings["Friday"]["FridayExchanges"];
List<string> fridayExchanges = configEntry.ParseExchangeKey();
return (fridayExchanges.Count, fridayExchanges.ToArray());
}
Leave apart all the variables and names, the interesting one is MondaySettings. It is defined like this:
using IniParser;
using IniParser.Model;
/// <summary>
/// Holds the dictionary of Settings in IniData format.
/// </summary>
public static IniData MondaySettings { get; private set; }
and initialized like this:
[MemberNotNull(nameof(MondaySettings))]
private static void ReadSettingsFile(string settingsFile)
{
var iniParser = new FileIniDataParser();
MondaySettings = iniParser.ReadFile(settingsFile);
if (!DateTime.TryParse(MondaySettings["General"]["LastShutDown"], out _))
{
CorrectLastShutDownTime();
}
}
you find all nice calls in the ini-parser package to read and write the ini files automatically with almost one-liners.
When saving the file before closing and exiting:
private static bool SaveInternalSettings(string settingsFilename)
{
string settingsFile = Path.Combine(SettingsLocationFullPath, settingsFilename);
MondaySettings["General"]["LastShutDown"] = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture);
File.Delete(settingsFile);
try
{
var parser = new FileIniDataParser();
parser.WriteFile(settingsFile, MondaySettings);
return true;
}
catch
{
return false;
}
}
Now the interesting part: parsing multiple rhs values within ini. My solution was to do it manually, which is also one of the fastest ways.
/// <summary>
/// Reading Settings.ini for multi value lines, this routine parses the right hand side
/// of each records to define the list of values coupling it with the required
/// exchanges. Returns an array of exchanges that may be contained in a complex key to
/// resolve the All case and the presence of the pipe symbol (|).
/// NOTE: it makes use of the indexes for markets, therefore those structures should be
/// ready before this call to the method.
/// </summary>
/// <param name="rhrMarket">
/// The original key as written in the .ini file.
/// </param>
/// <returns>
/// A list of strings representing all the exchanges in a single record of
/// InterestingMarkets section the settings. If only one is contained in the original
/// key, a single element is contained.
/// </returns>
public static List<string> ParseExchangeKey(this string rhrMarket)
{
List<string> answer = new();
if (string.Equals(rhrMarket, "all", StringComparison.OrdinalIgnoreCase))
{
// producing all known exchanges as answer
foreach (KeyValuePair<string, int> ex in Tags.Exchanges.ActiveExchanges)
{
if (ex.Value != 0) // to exclude Monday which is id=0
{
answer.Add(ex.Key);
}
}
}
else if (rhrMarket.Contains('|'))
{
// sorting multiple cases
string[] split = rhrMarket.Split('|');
foreach (string? subElement in split)
{
answer.Add(subElement.ToLower().Trim());
}
}
else
{
answer.Add(rhrMarket.ToLower());
}
return answer;
}
There is also some XML to help you get to the point. As you can see, ParseExchangeKey returns a list of strings.
Just to make sure you have all the elements to get the Ienumerables, this is the definition of Tags.Exchanges.ActiveExchanges:
/// <summary>
/// Contains the set of active exchanges included into [ActiveExchanges] section in
/// settings.ini and having proper credentials to activate the web account. The
/// Values of the dictionary are the ID in _allExchanges. Monday is not included.
/// </summary>
[PublicAPI]
public static Dictionary<string, int> ActiveExchanges => _b_activeExchanges_;
/// <summary>
/// IMPL: This is internal because it needs to be set by <see cref="MondayConfiguration"/>.
/// it does not contain Monday's entry.
/// </summary>
private static readonly Dictionary<string, int> _b_activeExchanges_ = LoadExchangesDictionary();
private static Dictionary<string, int> LoadExchangesDictionary()
{
Dictionary<string, int> answer = new();
// set it internally without using the property which blocks the caller up the
// closure of the dictionary.
var counter = 0;
foreach (KeyValuePair<string, int> item in _allExchanges_)
{
if (item.Value != 0 && MondayConfiguration.Credentials.Any(c => c.Exchange == item.Key))
{
answer.Add(item.Key.ToLower(), item.Value);
counter++;
}
}
Saving is the same, however ini-parser leaves the original string with separators (pipes in this case) in its memory, if your used modified the set of multi-values, you just need to provide a simple ToString() version to concatenate it back before saving the file using the accustomed separator.
Then coupling with the controls of the Windows Forms is easy when you can get the List<string> object to move around.
This way you may skip all the custom classes for converting the type around which will likely slow down maintenance when you need to add or remove records from the ini file.
I would like to have a serialization format that is nearly identical to JSON, except that key-values are represented as <key>="<value>" instead of "<key>":"<value>".
With Newtonsoft I made a custom JsonConverter called TsonConverter that works fairly well, except that it can't "see" an embedded dictionary. Given the following type:
public class TraceyData
{
[Safe]
public string Application { get; set; }
[Safe]
public string SessionID { get; set; }
[Safe]
public string TraceID { get; set; }
[Safe]
public string Workflow { get; set; }
[Safe]
public Dictionary<string, string> Tags {get; set; }
[Safe]
public string[] Stuff {get; set;}
}
And the following code:
TsonConverter weird = new TsonConverter();
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.NullValueHandling = NullValueHandling.Ignore;
settings.Converters.Add(weird);
var tracey = new TraceyData();
tracey.TraceID = Guid.NewGuid().ToString();
tracey.SessionID = "5";
tracey.Tags["Referrer"] = "http://www.sky.net/deals";
tracey.Stuff = new string[] { "Alpha", "Bravo", "Charlie" };
tracey.Application = "Responsive";
string stuff = JsonConvert.SerializeObject(tracey, settings);
I get this:
[Application="Responsive" SessionID="5" TraceID="082ef853-92f8-4ce8-9f32-8e4f792fb022" Tags={"Referrer":"http://www.sky.net/deals"} Stuff=["Alpha","Bravo","Charlie"]]
Obviously I have also overridden the StartObject/EndObject notation, replacing { } with [ ]. Otherwise the results are not bad.
However, there is still the problem of the internal dictionary. In order
to convert the dictionary as well to use my <key>="<value>" format, it looks like I must make a deep dictionary converter.
I'm wondering if there is an easier way to do this.
Perhaps the Newtonsoft tool has a "property generator" and "key-value" generator property that I can set that globally handles this for me?
Any suggestions?
And while we're here, I wonder if there is a StartObject/EndObject formatter property override I can set, which would handle the other customization I've shown above. It would be nice to "skip" making JsonConverter tools for these kinds of simple alterations.
Incidentally:
My custom JsonConverter is choosing properties to serialize based on the [Safe] attribute shown in my sample. This is another nice-to-have. It would be wonderful if the JSon settings could expose an "attribute handler" property that lets me override the usual JSon attributes in favor of my own.
I have no need to de-serialize this format. It is intended as a one-way operation. If someone wishes also to explain how to de-serialize my custom format as well that is an interesting bonus, but definitely not necessary to answer this question.
Appendix
Below is the TraceConverter I had made. It references a FieldMetaData class that simply holds property info.
public class TsonConverter : JsonConverter
{
public override bool CanRead
{
get
{
return false;
}
}
public override bool CanConvert(Type ObjectType)
{
return DataClassifier.TestForUserType(ObjectType);
}
public override void WriteJson(
JsonWriter writer, object value, JsonSerializer serializer)
{
Type objType = value.GetType();
var props = objType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
var propMap = from p in props
from a in p.GetCustomAttributes(typeof(ProfileAttribute), false)
select new FieldMetaData(p, (ProfileAttribute)a);
//writer.WriteStartObject();
writer.WriteStartArray();
bool loopStarted = true;
foreach(var prop in propMap){
object rawValue = prop.GetValue(value);
if (rawValue != null || serializer.NullValueHandling == NullValueHandling.Include)
{
string jsonValue = JsonConvert.SerializeObject(prop.GetValue(value), this);
if (loopStarted)
{
loopStarted = false;
writer.WriteRaw(String.Format("{0}={1}", prop.Name, jsonValue));
}
else
{
writer.WriteRaw(String.Format(" {0}={1}", prop.Name, jsonValue));
}
}
//writer.WriteRaw(String.Format("{0}={1}", prop.Name, prop.GetValue(value)));
//writer.WritePropertyName(prop.Name, false);
//writer.WriteValue(prop.GetValue(value));
}
writer.WriteEndArray();
}
public override object ReadJson(
JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Rather than creating your own converter, you're going to need to create your own subclass of JsonWriter that writes to your custom file format. (This is how Json.NET implements its BsonWriter.) In your case, your file format is close enough to JSON that you can inherit from JsonTextWriter:
public class TsonTextWriter : JsonTextWriter
{
TextWriter _writer;
public TsonTextWriter(TextWriter textWriter)
: base(textWriter)
{
if (textWriter == null)
throw new ArgumentNullException("textWriter");
QuoteName = false;
_writer = textWriter;
}
public override void WriteStartObject()
{
SetWriteState(JsonToken.StartObject, null);
_writer.Write('[');
}
protected override void WriteEnd(JsonToken token)
{
switch (token)
{
case JsonToken.EndObject:
_writer.Write(']');
break;
default:
base.WriteEnd(token);
break;
}
}
public override void WritePropertyName(string name)
{
WritePropertyName(name, true);
}
public override void WritePropertyName(string name, bool escape)
{
SetWriteState(JsonToken.PropertyName, name);
var escaped = name;
if (escape)
{
escaped = JsonConvert.ToString(name, '"', StringEscapeHandling);
escaped = escaped.Substring(1, escaped.Length - 2);
}
// Maybe also escape the space character if it appears in a name?
_writer.Write(escaped.Replace("=", #"\u003d"));// Replace "=" with unicode escape sequence.
_writer.Write('=');
}
/// <summary>
/// Writes the JSON value delimiter. (Remove this override if you want to retain the comma separator.)
/// </summary>
protected override void WriteValueDelimiter()
{
_writer.Write(' ');
}
/// <summary>
/// Writes an indent space.
/// </summary>
protected override void WriteIndentSpace()
{
// Do nothing.
}
}
Having done this, now all classes will be serialized to your custom format when you use this writer, for instance:
var tracey = new TraceyData();
tracey.TraceID = Guid.NewGuid().ToString();
tracey.SessionID = "5";
tracey.Tags["Referrer"] = "http://www.sky.net/deals";
tracey.Stuff = new string[] { "Alpha", "Bravo", "Charlie" };
tracey.Application = "Responsive";
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.NullValueHandling = NullValueHandling.Ignore;
using (var sw = new StringWriter())
{
using (var jsonWriter = new TsonTextWriter(sw))
{
JsonSerializer.CreateDefault(settings).Serialize(jsonWriter, tracey);
}
Debug.WriteLine(sw.ToString());
}
Produces the output
[Application="Responsive" SessionID="5" TraceID="2437fe67-9788-47ba-91ce-2e5b670c2a34" Tags=[Referrer="http://www.sky.net/deals"] Stuff=["Alpha" "Bravo" "Charlie"]]
As far as deciding whether to serialize properties based on the presence of a [Safe] attribute, that's sort of a second question. You will need to create your own ContractResolver and override CreateProperty, for instance as shown here: Using JSON.net, how do I prevent serializing properties of a derived class, when used in a base class context?
Update
If you want to retain the comma separator for arrays but not objects, modify WriteValueDelimiter as follows:
/// <summary>
/// Writes the JSON value delimiter. (Remove this override if you want to retain the comma separator.)
/// </summary>
protected override void WriteValueDelimiter()
{
if (WriteState == WriteState.Array)
_writer.Write(',');
else
_writer.Write(' ');
}
Intro:
Web application, ASP.NET MVC 3, a controller action that accepts an instance of POCO model class with (potentially) large field.
Model class:
public class View
{
[Required]
[RegularExpression(...)]
public object name { get; set; }
public object details { get; set; }
public object content { get; set; } // the problem field
}
Controller action:
[ActionName(...)]
[Authorize(...)]
[HttpPost]
public ActionResult CreateView(View view)
{
if (!ModelState.IsValid) { return /*some ActionResult here*/;}
... //do other stuff, create object in db etc. return valid result
}
Problem:
An action should be able to accept large JSON objects (at least up to hundred megabytes in a single request and that's no joke). By default I met with several restrictions like httpRuntime maxRequestLength etc. - all solved except MaxJsonLengh - meaning that default ValueProviderFactory for JSON is not capable of handling such objects.
Tried:
Setting
<system.web.extensions>
<scripting>
<webServices>
<jsonSerialization maxJsonLength="2147483647"/>
</webServices>
</scripting>
</system.web.extensions>
does not help.
Creating my own custom ValueProviderFactory as described in #Darin's answer here:
JsonValueProviderFactory throws "request too large"
also failed because I have no possibility to use JSON.Net (due to non-technical reasons). I tried to implement correct deserialization here myself but apparently it's a bit above my knowledge (yet). I was able to deserialize my JSON string to Dictionary<String,Object> here, but that's not what I want - I want to deserialize it to my lovely POCO objects and use them as input parameters for actions.
So, the questions:
Anyone knows better way to overcome the problem without implementing universal custom ValueProviderFactory?
Is there a possibility to specify for what specific controller and action I want to use my custom ValueProviderFactory? If I know the action beforehand than I will be able to deserialize JSON to POCO without much coding in ValueProviderFactory...
I'm also thinking about implementing a custom ActionFilter for that specific problem, but I think it's a bit ugly.
Anyone can suggest a good solution?
The built-in JsonValueProviderFactory ignores the <jsonSerialization maxJsonLength="50000000"/> setting. So you could write a custom factory by using the built-in implementation:
public sealed class MyJsonValueProviderFactory : ValueProviderFactory
{
private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
{
IDictionary<string, object> d = value as IDictionary<string, object>;
if (d != null)
{
foreach (KeyValuePair<string, object> entry in d)
{
AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
}
return;
}
IList l = value as IList;
if (l != null)
{
for (int i = 0; i < l.Count; i++)
{
AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
}
return;
}
// primitive
backingStore[prefix] = value;
}
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
// not JSON request
return null;
}
StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
string bodyText = reader.ReadToEnd();
if (String.IsNullOrEmpty(bodyText))
{
// no JSON data
return null;
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
serializer.MaxJsonLength = 2147483647;
object jsonData = serializer.DeserializeObject(bodyText);
return jsonData;
}
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
object jsonData = GetDeserializedObject(controllerContext);
if (jsonData == null)
{
return null;
}
Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
AddToBackingStore(backingStore, String.Empty, jsonData);
return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
}
private static string MakeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
}
private static string MakePropertyKey(string prefix, string propertyName)
{
return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
}
}
The only modification I did compared to the default factory is adding the following line:
serializer.MaxJsonLength = 2147483647;
Unfortunately this factory is not extensible at all, sealed stuff so I had to recreate it.
and in your Application_Start:
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<System.Web.Mvc.JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new MyJsonValueProviderFactory());
I found that the maxRequestLength did not solve the problem however.
I resolved my issue with the below setting. It is cleaner than having to implement a custom ValueProviderFactory
<appSettings>
<add key="aspnet:MaxJsonDeserializerMembers" value="150000" />
</appSettings>
Credit goes to the following questions:
JsonValueProviderFactory throws "request too large"
Getting "The JSON request was too large to be deserialized"
This setting obviously relates to a highly complex json model and not the actual size.
The solution of Darin Dimitrov works for me but i need reset the position of the stream of the request before read it, adding this line:
controllerContext.HttpContext.Request.InputStream.Position = 0;
So now, the method GetDeserializedObject looks like this:
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
// not JSON request
return null;
}
controllerContext.HttpContext.Request.InputStream.Position = 0;
StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
string bodyText = reader.ReadToEnd();
if (String.IsNullOrEmpty(bodyText))
{
// no JSON data
return null;
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
serializer.MaxJsonLength = 2147483647;
object jsonData = serializer.DeserializeObject(bodyText);
return jsonData;
}
What is the best way to add multiple classes to a tag using HtmlTextWriter?
What I would like to do is something like...
writer.AddAttribute(HtmlTextWriterAttribute.Class, "Class1");
writer.AddAttribute(HtmlTextWriterAttribute.Class, "Class2");
writer.RenderBeginTag(HtmlTextWriterTag.Table);
Resulting in...
<table class="Class1 Class2">
I appreciate I could do...
writer.AddAttribute(HtmlTextWriterAttribute.Class, "Class1 Class2");
However it isn't always this straightforward when building the control dynamically. Are there alternative ways to "append" classes to the tag?
why not extend the writer class and add a AddClass and RemoveClass methods on it which while rendering uses all added class names. Internally you could use a List _classNames to hold then and later just join them
writer.AddAttribute(HtmlTextWriterAttribute.Class,string.Join(_classNames.ToArray(), " ");
Hope that helps!
Just following the previous post idea....
public class NavHtmlTextWritter : HtmlTextWriter
{
private Dictionary<HtmlTextWriterAttribute, List<string>> attrValues = new Dictionary<HtmlTextWriterAttribute, List<string>>();
private HtmlTextWriterAttribute[] multiValueAttrs = new[] { HtmlTextWriterAttribute.Class };
public NavHtmlTextWritter (TextWriter writer) : base(writer) { }
public override void AddAttribute(HtmlTextWriterAttribute key, string value)
{
if (multiValueAttrs.Contains(key))
{
if (!this.attrValues.ContainsKey(key))
this.attrValues.Add(key, new List<string>());
this.attrValues[key].Add(value);
}
else
{
base.AddAttribute(key, value);
}
}
public override void RenderBeginTag(HtmlTextWriterTag tagKey)
{
this.addMultiValuesAttrs();
base.RenderBeginTag(tagKey);
}
public override void RenderBeginTag(string tagName)
{
this.addMultiValuesAttrs();
base.RenderBeginTag(tagName);
}
private void addMultiValuesAttrs()
{
foreach (var key in this.attrValues.Keys)
this.AddAttribute(key.ToString(), string.Join(" ", this.attrValues[key].ToArray()));
this.attrValues = new Dictionary<HtmlTextWriterAttribute, List<string>>();
}
}
I need to serialize objects to JSON. I would like to do it with a template instead of using data annotations (as most frameworks do). Does anybody know a good way of doing this?
A picture says more than 1000 words. I'm looking for something that looks like this:
For example, if I had a class like this:
public class Test
{
public string Key { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public Test Related { get; set; }
}
And a had template string that could look like this:
{
id: "$Key",
name: "$Name",
related: "$Related.Name"
}
I want to get a JSON object, whose properties are filled in according to Key, Name and Related.Name of the object.
Basically I'm searching for a JSON serialization method that supports templating instead.
I don't know about any library that does this for you, but it's not that hard to build it yourself.
If you have your template, you need to parse it as JSON and then replace all of the placeholders with actual values. To do that, you can use the visitor pattern.
Since JSON.NET (the JSON library I'm using) doesn't seem to have a visitor, you can create one yourself:
abstract class JsonVisitor
{
public virtual JToken Visit(JToken token)
{
var clone = token.DeepClone();
return VisitInternal(clone);
}
protected virtual JToken VisitInternal(JToken token)
{
switch (token.Type)
{
case JTokenType.Object:
return VisitObject((JObject)token);
case JTokenType.Property:
return VisitProperty((JProperty)token);
case JTokenType.Array:
return VisitArray((JArray)token);
case JTokenType.String:
case JTokenType.Integer:
case JTokenType.Float:
case JTokenType.Date:
case JTokenType.Boolean:
case JTokenType.Null:
return VisitValue((JValue)token);
default:
throw new InvalidOperationException();
}
}
protected virtual JToken VisitObject(JObject obj)
{
foreach (var property in obj.Properties())
VisitInternal(property);
return obj;
}
protected virtual JToken VisitProperty(JProperty property)
{
VisitInternal(property.Value);
return property;
}
protected virtual JToken VisitArray(JArray array)
{
foreach (var item in array)
VisitInternal(item);
return array;
}
protected virtual JToken VisitValue(JValue value)
{
return value;
}
}
And then create a specialized visitor that replaces the placeholders with actual values:
class JsonTemplateVisitor : JsonVisitor
{
private readonly object m_data;
private JsonTemplateVisitor(object data)
{
m_data = data;
}
public static JToken Serialize(object data, string templateString)
{
return Serialize(
data, (JToken)JsonConvert.DeserializeObject(templateString));
}
public static JToken Serialize(object data, JToken template)
{
var visitor = new JsonTemplateVisitor(data);
return visitor.Visit(template);
}
protected override JToken VisitValue(JValue value)
{
if (value.Type == JTokenType.String)
{
var s = (string)value.Value;
if (s.StartsWith("$"))
{
string path = s.Substring(1);
var newValue = GetValue(m_data, path);
var newValueToken = new JValue(newValue);
value.Replace(newValueToken);
return newValueToken;
}
}
return value;
}
private static object GetValue(object data, string path)
{
var parts = path.Split('.');
foreach (var part in parts)
{
if (data == null)
break;
data = data.GetType()
.GetProperty(part)
.GetValue(data, null);
}
return data;
}
}
The usage is then simple. For example, with the following template:
{
id : "$Key",
name: "$Name",
additionalInfo:
{
related: [ "$Related.Name" ]
}
}
You can use code like this:
JsonTemplateVisitor.Serialize(data, templateString)
The result then looks like this:
{
"id": "someKey",
"name": "Isaac",
"additionalInfo": {
"related": [
"Arthur"
]
}
}
You might want to add some error-checking, but other than that, the code should work. Also, it uses reflection, so it might not be suitable if performance is important.
10 years have passed since I've posted the question. Since I've been working with Node.JS and discovered Handlebars and how it is pretty easy to get it to parse JSON instead of HTML template. The Handlebars project has been converted to .NET.
You can use a special ITextEncoder to let Handlebars generate JSON:
using HandlebarsDotNet;
using System.Text;
public class JsonTextEncoder : ITextEncoder
{
public void Encode(StringBuilder text, TextWriter target)
{
Encode(text.ToString(), target);
}
public void Encode(string text, TextWriter target)
{
if (text == null || text == "") return;
text = System.Web.HttpUtility.JavaScriptStringEncode(text);
target.Write(text);
}
public void Encode<T>(T text, TextWriter target) where T : IEnumerator<char>
{
var str = text?.ToString();
if (str == null) return;
Encode(str, target);
}
}
Let's see it in action:
using HandlebarsDotNet;
var handlebars = Handlebars.Create();
handlebars.Configuration.TextEncoder = new JsonTextEncoder();
var sourceTemplate = #"{
""id"": ""{{Key}}"",
""name"": ""{{Name}}"",
""related "": ""{{Related.Name}}""
}";
var template = handlebars.Compile(sourceTemplate);
var json = template(new
{
Key = "Alpha",
Name = "Beta",
Related = new
{
Name = "Gamme"
}
});
Console.WriteLine(json);
This will write the following:
{
"id": "Alpha",
"name": "Beta",
"related ": "Gamme"
}
I did a small write-up on the topic on my blog: Handlebars.Net & JSON templates. In this blog I also discuss how to improve debugging these templates.
You can also use a Text Template file for your json template . The template engine will fill in the blanks and return you the result.
If you are using Visual Studio,
Create a .tt file ,
Mark it with TextTemplatingFilePreprocessor in Custom Tool property of the file. This will create a new class for you that takes care of processing the template.
For integrating your data in the resulted string , extend the newly generated class in a separate file , in which you pass the data (the arbitrary class from you image).
Use this to get the json formatted code;
MyData data = ...;
MyTemplatePage page = new MyTemplatePage(data);
String pageContent = page.TransformText();
Now the pageContent have the json formatted string; For more details about how to handle the .tt file , look here : Text Template Control Blocks
I had exactly the same need. I needed an end user (technical users but not developers) to be able to create their own json files that can later be filled via data.
Microsoft Teams is doing something similar with their adaptive card website:
https://adaptivecards.io/designer/
On the bottom left there is a json "template" and on the bottom right a json to load into the template.
Conclusion: Despite extensive research I have not found any .NET library doing this.
Sorry (๑•́ㅿ•̀๑).
Screenshot of adaptive card designer