How can this code be optimized? - c#

I have a method which in essence converts a datatable to a list of objects which I call 'Bags'. This code is called many times per session, with many sessions concurrently running and with sometimes thousands of rows. Because of this I need it to be as quick as possible. I have an xml file which contains the DataColumn to Property mappings. The main method to optimize is ConvertRowToBag - the type parameter passed in is a type which derives from BagBase.
It's a long bit of code, but any tips would be much appreciated.
public class BagBase
{
/// <summary>
/// Dictionary of properties and names
/// </summary>
private static Dictionary<string, PropertyInfo> propertyDictionary = new Dictionary<string, PropertyInfo>();
/// <summary>
/// Table of column/property mappings
/// </summary>
private static DataTable mappings = new DataTable("Mappings");
/// <summary>
/// Returns true if the map exists
/// </summary>
/// <param name="columnName"></param>
/// <param name="type"></param>
/// <returns></returns>
private static bool MappingExists(string columnName, Type type)
{
DataRow [] rows = BagBase.mappings.Select(String.Format("Type = '{0}' and ColumnName = '{1}'", type.Name, columnName));
return (rows != null && rows.Length > 0);
}
/// <summary>
/// Converts the table to bags.
/// </summary>
/// <param name="table">The table.</param>
/// <param name="outputType">Type of the output.</param>
/// <returns></returns>
protected static List<BagBase> ConvertTableToBags(DataTable table, Type outputType)
{
Trace.TraceInformation(String.Format("ConvertTableToBags : table={0} Type={1}", table.TableName, outputType.Name));
// Create an empty list
List<BagBase> result = new List<BagBase>();
// Iterate through the rows
foreach (DataRow row in table.Rows)
{
// Add to the list
result.Add(ConvertRowToBag(outputType, row));
}
Trace.TraceInformation("ConvertTableToBags Finished.");
return result;
}
/// <summary>
/// Converts the row to bag.
/// </summary>
/// <param name="outputType">Type of the output.</param>
/// <param name="row">The row.</param>
/// <returns></returns>
protected static BagBase ConvertRowToBag(Type outputType, DataRow row)
{
// Create an instance of the child class and store in the base
BagBase bag = Activator.CreateInstance(outputType) as BagBase;
// Iterate through the columns
foreach (DataColumn column in row.Table.Columns)
{
// If this column has been mapped
if (BagBase.MappingExists(column.ColumnName, outputType))
{
PropertyInfo property;
string columnProperty = String.Format("{0}={1}", column.ColumnName, outputType.Name);
// Get the property as defined in the map
if (!propertyDictionary.ContainsKey(columnProperty))
{
// Get the property
property = outputType.GetProperty(BagBase.GetColumnMapping(column.ColumnName, outputType));
// Add the property to the dictionary
propertyDictionary.Add(columnProperty, property);
}
else
{
property = propertyDictionary[columnProperty];
}
if (property != null)
{
if (!row.IsNull(column))
{
// Set the value to the in the table
if (property.PropertyType.BaseType != null && property.PropertyType.BaseType == typeof(Enum))
{
if (column.DataType != typeof(String))
{
property.SetValue(bag, Enum.ToObject(property.PropertyType, row[column]), null);
}
else
{
property.SetValue(bag, Enum.ToObject(property.PropertyType, Convert.ToChar(row[column])), null);
}
}
else if (property.PropertyType == typeof(DateTime?))
{
property.SetValue(bag, (DateTime?)row[column], null);
}
else
{
property.SetValue(bag, Convert.ChangeType(row[column], property.PropertyType), null);
}
}
else // No nulls
{
if (column.DataType == typeof(String))
{
property.SetValue(bag, String.Empty, null);
}
}
// Generate the unique class.property name
string propertyKey = String.Format("{0}.{1}", outputType.Name, property.Name);
if (!columnCaptions.ContainsKey(propertyKey))
{
// Add to the caption map
columnCaptions.Add(propertyKey, column.Caption);
}
}
}
else
{
// If this column isn't mapped, add it to Other information
if (bag.OtherInformation == null)
{
bag.OtherInformation = new Dictionary<string, string>();
}
bag.OtherInformation.Add(column.ColumnName, !row.IsNull(column) ? row[column].ToString() : String.Empty);
}
}
return bag;
}
}

Use a profiler. There's no way for us to know what actually takes the most time in your code.
There's just really no use trying to optimize line-by-line and many people seem to not know this. Computers are always waiting for a resource, sometimes it's CPU or disk IO, and often it's the user. To make any piece of code faster, find the bottlenecks using a profiler and work on making that code faster.

Aside from the general advise of "use a profiler", there is probably not one bottleneck but either a series of slow calls or the very structure of the procedure is creating unnecessary iterations. At a glance:
The Select against a datatable is generally not very performant.
Reflection carries with it a lot of overhead, it looks like you are dependent on it but if you could limit its scope you will probably get better overall performance.

Related

Is this an efficient way of bulk inserting using Dapper?

Is this an efficient way of bulk inserting using Dapper?
Also, is this more efficient than creating a stored procedure and passing models to it?
Is this an efficient way of bulk inserting using Dapper?
Also, is this more efficient than creating a stored procedure and passing models to it?
category = new Category
{
Name = "category",
Description = "description",
Created = null,
LastModified = null,
CategoryPictures = new CategoryPicture[]
{
new CategoryPicture
{
CategoryId = 3,
PictureId = 2,
Picture = new Picture
{
Url = "newUrl"
}
},
new CategoryPicture
{
CategoryId = 3,
PictureId = 2,
Picture = new Picture
{
Url = "url"
}
}
}
};
string sql = #"INSERT INTO Categories(Name, Description, Created, LastModified)
VALUES(#Name, #Description, #Created, #LastModified)";
await conn.ExecuteAsync(sql, new
{
category.Name,
category.Description,
category.Created,
category.LastModified
});
string catPicInsert = #"INSERT INTO CategoryPictures(fk_CategoryId, fk_PictureId)
VALUES(#CategoryId, #PictureId)";
await conn.ExecuteAsync(catPicInsert, category.CategoryPictures);
string PicInsert = #"INSERT INTO Pictures(Url)
VALUES(#Url)";
await conn.ExecuteAsync(PicInsert, category.CategoryPictures.Select(x => x.Picture).ToList());
It won't be hugely slow, but it won't be anywhere near as fast as a bulk copy. Options, assuming SQL Server:
it is possible to use TVPs with Dapper, but the only convenient way to do this is by packing your input data into a DataTable; there are examples of TVP usage in the Dapper repo, or I can knock one out, but they're inconvenient because you need to declare the parameter type at the server
you can use SqlBulkCopy to throw data into the database independent of Dapper; FastMember has ObjectReader that can construct an IDataReader over a typed sequence, suitable for use withSqlBulkCopy
If you're not using SQL Server, you'll need to look at vendor-specific options for your RDBMS.
I've changed it up into using multiple sprocs for each table.
public async Task<bool> CreateCategoryAsync(Category category)
{
category = new Category
{
Name = "category",
Description = "description",
Created = null,
LastModified = null,
CategoryPictures = new CategoryPicture[]
{
new CategoryPicture
{
CategoryId = 1,
PictureId = 2,
Picture = new Picture
{
Url = "newUrl"
}
},
new CategoryPicture
{
CategoryId = 2,
PictureId = 2,
Picture = new Picture
{
Url = "url"
}
}
}
};
string sql = #"EXEC Categories_Insert #Categories;
EXEC CategoryPictures_Insert #CategoryPictures;
EXEC Pictures_Insert #Pictures";
var spParams = new DynamicParameters(new
{
Categories = category.ToDataTable().AsTableValuedParameter("CategoriesType"),
CategoryPictures = category.CategoryPictures.ListToDataTable()
.AsTableValuedParameter("CategoryPictureType"),
Pictures = category.CategoryPictures.Select(x => x.Picture)
.ListToDataTable()
.AsTableValuedParameter("PicturesType")
});
using (var conn = SqlConnection())
{
using (var res = await conn.QueryMultipleAsync(sql, spParams))
{
return true;
}
}
}
This is the extension class with generic methods that i created for mapping objects or a list of objects into Datatable
public static class DataTableExtensions
{
/// <summary>
/// Convert an IEnumerable into a Datatable
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="listToDataTable"></param>
/// <returns></returns>
public static DataTable ListToDataTable<T>(this IEnumerable<T> listToDataTable)
{
DataTable dataTable = new DataTable();
AddToDataTableColumns<T>(dataTable);
foreach (var item in listToDataTable)
{
AddDataTableRows(dataTable, item);
}
return dataTable;
}
/// <summary>
/// Comvert a Type of Class to DataTable
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="obj"></param>
/// <returns></returns>
public static DataTable ToDataTable<T>(this T obj) where T : class
{
DataTable dataTable = new DataTable();
AddToDataTableColumns<T>(dataTable);
AddDataTableRows(dataTable, obj);
return dataTable;
}
/// <summary>
/// Add values from of Type T to a given Datatable columns
/// </summary>
/// <typeparam name="T">Extract values from</typeparam>
/// <param name="dataTable">The datatable to add column values into</param>
/// <returns></returns>
private static DataTable AddToDataTableColumns<T>(DataTable dataTable)
{
try
{
PropertyInfo[] FilteredProps = GetFilteredProperties(typeof(T));
for (int i = 0; i < FilteredProps.Length; i++)
{
PropertyInfo prop = FilteredProps[i];
Type type = prop.PropertyType;
dataTable.Columns.Add(prop.Name, Nullable.GetUnderlyingType(type) ?? type);
}
}
catch (Exception ex)
{
new InfrastructuredException(ex.StackTrace);
}
return dataTable;
}
/// <summary>
/// Add values from of Type T to a given Datatable Rows
/// </summary>
/// <typeparam name="T">Extract values from</typeparam>
/// <param name="dataTable">The datatable to add Row values into</param>
/// <returns></returns>
private static DataTable AddDataTableRows<T>(DataTable dataTable, T obj)
{
try
{
PropertyInfo[] FilteredProps = GetFilteredProperties(typeof(T));
object[] values = new object[FilteredProps.Length];
for (int i = 0; i < values.Length; i++)
{
values[i] = FilteredProps[i].GetValue(obj);
}
dataTable.Rows.Add(values);
}
catch (Exception ex)
{
new InfrastructuredException(ex.StackTrace);
}
return dataTable;
}
/// <summary>
/// Return an array of Filterered Properties of a Type
/// </summary>
/// <param name="type"></param>
/// <returns>Properties that are filtered by Type</returns>
private static PropertyInfo[] GetFilteredProperties(Type type)
{
return type.GetProperties()
.Where(p => p.Name != "Id" && !p.PropertyType.IsSubclassOf(typeof(BaseEntity)) && !p.PropertyType.IsInterface)
.ToArray();
}
}
I would suggest that you use Bulk Insert function of SQL.
It can be combined with Dapper as well.
I was looking for the example like this for a long time, and finally found the pieces of solution, and combined them in one place.
You can see the example in my repo:
https://github.com/ayrat162/BulkInsert
It uses Dapper.Contrib, FastMember, and SqlBulkCopy for uploading large chunks of data to MS SQL Server.

SQL Server CLR function aggregation of sorted string

In order to get a sorted aggregated string, I wrote the CLR function below. However, it always returns empty instead of what I expected, just like "001, 002, 003". I tried to debug the CLR function in visual studio 2017, but threw the error message
The operation could not be completed. Unspecified error
Code:
[Serializable]
[SqlUserDefinedAggregate(
Format.UserDefined, //use clr serialization to serialize the intermediate result
Name = "CLRSortedCssvAgg", //aggregate name on sql
IsInvariantToNulls = true, //optimizer property
IsInvariantToDuplicates = false, //optimizer property
IsInvariantToOrder = false, //optimizer property
IsNullIfEmpty = false, //optimizer property
MaxByteSize = -1) //maximum size in bytes of persisted value
]
public class SortedCssvConcatenateAgg : IBinarySerialize
{
/// <summary>
/// The variable that holds all the strings to be aggregated.
/// </summary>
List<string> aggregationList;
StringBuilder accumulator;
/// <summary>
/// Separator between concatenated values.
/// </summary>
const string CommaSpaceSeparator = ", ";
/// <summary>
/// Initialize the internal data structures.
/// </summary>
public void Init()
{
accumulator = new StringBuilder();
aggregationList = new List<string>();
}
/// <summary>
/// Accumulate the next value, not if the value is null or empty.
/// </summary>
public void Accumulate(SqlString value)
{
if (value.IsNull || String.IsNullOrEmpty(value.Value))
{
return;
}
aggregationList.Add(value.Value);
}
/// <summary>
/// Merge the partially computed aggregate with this aggregate.
/// </summary>
/// <param name="other"></param>
public void Merge(SortedCssvConcatenateAgg other)
{
aggregationList.AddRange(other.aggregationList);
}
/// <summary>
/// Called at the end of aggregation, to return the results of the aggregation.
/// </summary>
/// <returns></returns>
public SqlString Terminate()
{
if (aggregationList != null && aggregationList.Count > 0)
{
aggregationList.Sort();
accumulator.Append(string.Join(CommaSpaceSeparator, aggregationList));
aggregationList.Clear();
}
return new SqlString(accumulator.ToString());
}
public void Read(BinaryReader r)
{
accumulator = new StringBuilder(r.ReadString());
}
public void Write(BinaryWriter w)
{
w.Write(accumulator.ToString());
}
}
You are close. Just need a few minor adjustments. Do the following and it will work (I tested it):
Remove all references to accumulator. It is not used.
Replace the Terminate(), Read(), and Write() methods with the following:
public SqlString Terminate()
{
string _Aggregation = null;
if (aggregationList != null && aggregationList.Count > 0)
{
aggregationList.Sort();
_Aggregation = string.Join(CommaSpaceSeparator, aggregationList);
}
return new SqlString(_Aggregation);
}
public void Read(BinaryReader r)
{
int _Count = r.ReadInt32();
aggregationList = new List<string>(_Count);
for (int _Index = 0; _Index < _Count; _Index++)
{
aggregationList.Add(r.ReadString());
}
}
public void Write(BinaryWriter w)
{
w.Write(aggregationList.Count);
foreach (string _Item in aggregationList)
{
w.Write(_Item);
}
}
That said, I'm not sure if this approach is faster or slower than the FOR XML approach, but a UDA certainly makes for a more readable query, especially if you need multiple aggregations.
Still, I should mention that starting in SQL Server 2017, this became a built-in function: STRING_AGG (which allows for sorting via the WITHIN GROUP (ORDER BY ... ) clause).
In your Accumulate and Merge, you're dealing with your aggregationList; in Read and Write you're dealing with accumulator. You should pick one or the other for all of them and use it. As I understand it, Read and Write are used when the engine needs to persist temporary results to a work table. For your case, when it does that, it's persisting only your empty StringBuilder.

c# mocking EF6 context and dealing with auto increment primary keys

Has anyone found a good way to get auto-increment primary keys in a mocked context to work when testing service layers?
In most cases, seeding the primary key as part of the data to test is possible. But many service layer methods deal with creating multiple objects or linking other processes together that quickly fail if you are not responsible for passing all of the created data in. I thought of maybe adding a Callback() to SaveChangesAsync() that looks at the data created, and auto-generates a primary key incrementally but it won't be simple to implement.
var organization = new PrivateOrganization();
organization.Name = "New Test Organization";
organization.Description = "New Test Organization description";
organization.OrganizationTypeId = ITNOrganizationTypes.Agency;
organization.OrganizationStatusTypeId = (int)ITNOrganizationStatusTypes.Enabled;
organization.ShortCode = "Test";
var newOrg = await _service.InsertPrivateOrganizationAsync(organization);
_mockPrivateOrganizationsSet.Verify(m => m.Add(It.IsAny<PrivateOrganization>()), Times.Once());
MockTenantContext.Verify(m => m.SaveChangesAsync(), Times.Once());
// validation passes, but contains no auto-generated primary key.
I had to develop my own solution for this as follows:
/// <summary>
/// A helper class for managing custom behaviors of Mockable database contexts
/// </summary>
public static partial class EFSaveChangesBehaviors
{
/// <summary>
/// Enable auto-incrementing of primary key values upon SaveChanges/SaveChangesAsync
/// </summary>
/// <typeparam name="T">The type of context to enable auto-incrementing on</typeparam>
/// <param name="context">The context to enable this feature</param>
public static void EnableAutoIncrementOnSave<T>(this Mock<T> context) where T : DbContext
{
context.Setup(m => m.SaveChangesAsync())
.Callback(() =>
{
EFSaveChangesBehaviors.SaveChangesIncrementKey(context.Object);
})
.Returns(() => Task.Run(() => { return 1; }))
.Verifiable();
context.Setup(m => m.SaveChanges())
.Callback(() =>
{
EFSaveChangesBehaviors.SaveChangesIncrementKey(context.Object);
})
.Returns(() => { return 1; })
.Verifiable();
}
/// <summary>
/// Implements key incrementing of data records that are pending to be added to the context
/// </summary>
/// <param name="context"></param>
public static void SaveChangesIncrementKey(DbContext context)
{
var tablesWithNewData = GetUnsavedRows<DbContext>(context);
for (int i = 0; i < tablesWithNewData.Count; i++)
{
long nextPrimaryKeyValue = 0;
var tableWithDataProperty = tablesWithNewData[i];
var tableWithDataObject = tableWithDataProperty.GetValue(context);
if (tableWithDataObject != null)
{
var tableWithDataQueryable = tableWithDataObject as IQueryable<object>;
// 1) get the highest value in the DbSet<> (table) to continue auto-increment from
nextPrimaryKeyValue = IterateAndPerformAction(context, tableWithDataQueryable, tableWithDataProperty, nextPrimaryKeyValue, (primaryExistingKeyValue, primaryKeyRowObject, primaryKeyProperty) =>
{
if (primaryExistingKeyValue > nextPrimaryKeyValue)
nextPrimaryKeyValue = Convert.ToInt64(primaryExistingKeyValue);
return nextPrimaryKeyValue;
});
// 2) increase the value of the record's primary key on each iteration
IterateAndPerformAction(context, tableWithDataQueryable, tableWithDataProperty, nextPrimaryKeyValue, (primaryKeyExistingValue, primaryKeyRowObject, primaryKeyProperty) =>
{
if (primaryKeyExistingValue == 0)
{
nextPrimaryKeyValue++;
Type propertyType = primaryKeyProperty.PropertyType;
if (propertyType == typeof(Int64))
primaryKeyProperty.SetValue(primaryKeyRowObject, nextPrimaryKeyValue);
else if (propertyType == typeof(Int32))
primaryKeyProperty.SetValue(primaryKeyRowObject, Convert.ToInt32(nextPrimaryKeyValue));
else if (propertyType == typeof(Int16))
primaryKeyProperty.SetValue(primaryKeyRowObject, Convert.ToInt16(nextPrimaryKeyValue));
else if (propertyType == typeof(byte))
primaryKeyProperty.SetValue(primaryKeyRowObject, Convert.ToByte(nextPrimaryKeyValue));
else
throw new System.NotImplementedException($"Cannot manage primary keys of type: {propertyType.FullName}");
}
return nextPrimaryKeyValue;
});
}
}
}
/// <summary>
/// Get a list of properties for a data table that are indicated as a primary key
/// </summary>
/// <param name="t"></param>
/// <param name="context"></param>
/// <returns></returns>
/// <remarks>Reflection must be used, as the ObjectContext is not mockable</remarks>
public static PropertyInfo[] GetPrimaryKeyNamesUsingReflection(Type t, DbContext context)
{
var properties = t.GetProperties();
var keyNames = properties
.Where(prop => Attribute.IsDefined(prop, typeof(System.ComponentModel.DataAnnotations.KeyAttribute)))
.ToArray();
return keyNames;
}
/// <summary>
/// Iterates a table's data and allows an action to be performed on each row
/// </summary>
/// <param name="context">The database context</param>
/// <param name="tableWithDataQueryable"></param>
/// <param name="tableWithDataProperty"></param>
/// <param name="nextPrimaryKeyValue"></param>
/// <param name="action"></param>
/// <returns></returns>
private static long IterateAndPerformAction(DbContext context, IQueryable<object> tableWithDataQueryable, PropertyInfo tableWithDataProperty, long nextPrimaryKeyValue, Func<long, object, PropertyInfo, long> action)
{
foreach (var primaryKeyRowObject in tableWithDataQueryable)
{
// create a primary key for the object
if (tableWithDataProperty.PropertyType.GenericTypeArguments.Length > 0)
{
var dbSetType = tableWithDataProperty.PropertyType.GenericTypeArguments[0];
// find the primary key property
var primaryKeyProperty = GetPrimaryKeyNamesUsingReflection(dbSetType, context).FirstOrDefault();
if (primaryKeyProperty != null)
{
var primaryKeyValue = primaryKeyProperty.GetValue(primaryKeyRowObject) ?? 0L;
nextPrimaryKeyValue = action(Convert.ToInt64(primaryKeyValue), primaryKeyRowObject, primaryKeyProperty);
}
}
}
return nextPrimaryKeyValue;
}
/// <summary>
/// Get a list of objects which are pending to be added to the context
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="context"></param>
/// <returns></returns>
private static IList<PropertyInfo> GetUnsavedRows<T>(T context)
{
// get list of properties of type DbSet<>
var dbSetProperties = new List<PropertyInfo>();
var properties = context.GetType().GetProperties();
foreach (var property in properties)
{
var setType = property.PropertyType;
var isDbSet = setType.IsGenericType && (typeof(IDbSet<>).IsAssignableFrom(setType.GetGenericTypeDefinition()) || setType.GetInterface(typeof(IDbSet<>).FullName) != null);
if (isDbSet)
{
dbSetProperties.Add(property);
}
}
return dbSetProperties;
}
}
Usage:
// enable auto-increment in our in-memory database
MockTenantContext.EnableAutoIncrementOnSave();

How can I send an array as a parameter of a query string in c#?

I am trying to use a home grown web API to retrieve some data. The documentation is all written in PHP. The example I'm looking at is this:
$params = array(
'id' => 1
,'data' => array(
,'email' => 'example#hasoffers.com'
)
$url = "www.someapi.com/api?" . http_build_query( $params );
I'm using the C# WebClient class, but I can't figure out how to serialize the data parameter:
WebClient wc = new WebClient();
wc.QueryString["id"] = "1";
wc.QueryString["data"] = // I have no idea.
string json = wc.DownloadString(apiUrl);
I've tried a few variations:
wc.QueryString["data"] = "email=test#stackoverflow.com";
wc.QueryString["data"] = Uri.EscapeDataString("data[email]=test#stackoverflow.com");
wc.QueryString["data"] = Uri.EscapeDataString("email[0]=test#stackoverflow.com");
wc.QueryString["data"] = Uri.EscapeDataString("email=test#stackoverflow.com");
Of course, I don't have PHP setup anywhere to see what the http_build_query() is actually returning.
I finally figured it out. I guess posting this question rebooted my brain.
This is what worked:
wc.QueryString["data[email]"] = "test#stackoverflow.com";
Note, this question is very relevant to this answer, so I put this class as an answer there as well. That answer will receive any updates to this class.
As far as I know, there's nothing built in to do this. But, you can create your own class.
So I did:
/// <summary>
/// Helps up build a query string by converting an object into a set of named-values and making a
/// query string out of it.
/// </summary>
public class QueryStringBuilder
{
private readonly List<KeyValuePair<string, object>> _keyValuePairs
= new List<KeyValuePair<string, object>>();
/// <summary> Builds the query string from the given instance. </summary>
public static string BuildQueryString(object queryData, string argSeperator = "&")
{
var encoder = new QueryStringBuilder();
encoder.AddEntry(null, queryData, allowObjects: true);
return encoder.GetUriString(argSeperator);
}
/// <summary>
/// Convert the key-value pairs that we've collected into an actual query string.
/// </summary>
private string GetUriString(string argSeperator)
{
return String.Join(argSeperator,
_keyValuePairs.Select(kvp =>
{
var key = Uri.EscapeDataString(kvp.Key);
var value = Uri.EscapeDataString(kvp.Value.ToString());
return $"{key}={value}";
}));
}
/// <summary> Adds a single entry to the collection. </summary>
/// <param name="prefix"> The prefix to use when generating the key of the entry. Can be null. </param>
/// <param name="instance"> The instance to add.
///
/// - If the instance is a dictionary, the entries determine the key and values.
/// - If the instance is a collection, the keys will be the index of the entries, and the value
/// will be each item in the collection.
/// - If allowObjects is true, then the object's properties' names will be the keys, and the
/// values of the properties will be the values.
/// - Otherwise the instance is added with the given prefix to the collection of items. </param>
/// <param name="allowObjects"> true to add the properties of the given instance (if the object is
/// not a collection or dictionary), false to add the object as a key-value pair. </param>
private void AddEntry(string prefix, object instance, bool allowObjects)
{
var dictionary = instance as IDictionary;
var collection = instance as ICollection;
if (dictionary != null)
{
Add(prefix, GetDictionaryAdapter(dictionary));
}
else if (collection != null)
{
Add(prefix, GetArrayAdapter(collection));
}
else if (allowObjects)
{
Add(prefix, GetObjectAdapter(instance));
}
else
{
_keyValuePairs.Add(new KeyValuePair<string, object>(prefix, instance));
}
}
/// <summary> Adds the given collection of entries. </summary>
private void Add(string prefix, IEnumerable<Entry> datas)
{
foreach (var item in datas)
{
var newPrefix = String.IsNullOrEmpty(prefix)
? item.Key
: $"{prefix}[{item.Key}]";
AddEntry(newPrefix, item.Value, allowObjects: false);
}
}
private struct Entry
{
public string Key;
public object Value;
}
/// <summary>
/// Returns a collection of entries that represent the properties on the object.
/// </summary>
private IEnumerable<Entry> GetObjectAdapter(object data)
{
var properties = data.GetType().GetProperties();
foreach (var property in properties)
{
yield return new Entry()
{
Key = property.Name,
Value = property.GetValue(data)
};
}
}
/// <summary>
/// Returns a collection of entries that represent items in the collection.
/// </summary>
private IEnumerable<Entry> GetArrayAdapter(ICollection collection)
{
int i = 0;
foreach (var item in collection)
{
yield return new Entry()
{
Key = i.ToString(),
Value = item,
};
i++;
}
}
/// <summary>
/// Returns a collection of entries that represent items in the dictionary.
/// </summary>
private IEnumerable<Entry> GetDictionaryAdapter(IDictionary collection)
{
foreach (DictionaryEntry item in collection)
{
yield return new Entry()
{
Key = item.Key.ToString(),
Value = item.Value,
};
}
}
}
Like so:
// Age=19&Name=John%26Doe&Values%5B0%5D=1&Values%5B1%5D=2&Values%5B2%5D%5Bkey1%5D=value1&Values%5B2%5D%5Bkey2%5D=value2
QueryStringBuilder.BuildQueryString(new
{
Age = 19,
Name = "John&Doe",
Values = new object[]
{
1,
2,
new Dictionary<string, string>()
{
{ "key1", "value1" },
{ "key2", "value2" },
}
},
});
// 0=1&1=2&2%5B0%5D=one&2%5B1%5D=two&2%5B2%5D=three&3%5Bkey1%5D=value1&3%5Bkey2%5D=value2
QueryStringBuilder.BuildQueryString(new object[]
{
1,
2,
new object[] { "one", "two", "three" },
new Dictionary<string, string>()
{
{ "key1", "value1" },
{ "key2", "value2" },
}
}
);

Custom path of the user.config

I manage my application-settings using the setting-designer in VS2008.
"The exact path of the user.config
files looks something like this:"
<Profile Directory>\<Company Name>\
<App Name>_<Evidence Type>_<Evidence Hash>\<Version>\user.config
Is there to a way to customize this path? I would prefer something like this:
<Profile Directory>\<Company Name>\
<App Name>\<Version>\user.config
I noticed that white-spaces were replaced by underscores in the "Company Name" in the new created folder ("Test Company" --> "Test_Company"). I really wish to turn off this behavior.
You know, I could write a new XML-based setting-handler, but I would like to use the setting-designer.
It hasn't been easy to find good info on implementing a custom settings provider so I'm including a complete implementation below (bottom.) The format of the user.config file is retained, as well as the functionality within the .settings designer. I'm certain there are parts that can be cleaned up a bit, so don't hassle me :)
Like others, I wanted to change the location of the user.config file and still get the fun and fanciness of working with the .settings files in the designer, including creating default values for new installations. Importantly, our app also already has other saved settings objects at a path (appData\local\etc) in which we have already decided, and we didn't want artifacts in multiple locations.
The code is much longer than I'd like it to be, but there is no SHORT answer that I could find. Though it seems somewhat painful just to be able to control the path, creating a custom settings provider in itself is still pretty powerful. One could alter the follwing implementation to store the data just about anywhere including a custom encrypted file, database, or interact with a web serivice.
From what I've read, Microsoft does not intend on making the path to the config file configurable. I'll take their word for it when they say allowing that would be scary. See (this) post. Alas, if you want to do it yourself you must implement your own SettingsProvider.
Here goes..
Add a reference in your project to System.Configuration, you'll need it to implement the SettingsProvider.
Easy bit...Create a class which implements SettingsProvider, use ctrl+. to help you out.
class CustomSettingsProvider : SettingsProvider
Another easy bit...Go to the code behind of your .settings file (there is a button in the designer) and decorate the class to point it to your implementation. This must be done to override the built in functionality, but it does not change how the designer works.(sorry the formatting here is weird)
[System.Configuration.SettingsProvider(typeof(YourCompany.YourProduct.CustomSettingsProvider))]
public sealed partial class Settings
{
//bla bla bla
}
Getting the path: There is a property called "SettingsKey" (e.g. Properties.Settings.Default.SettingsKey) I used this to store the path. I made the following property.
/// <summary>
/// The key this is returning must set before the settings are used.
/// e.g. <c>Properties.Settings.Default.SettingsKey = #"C:\temp\user.config";</c>
/// </summary>
private string UserConfigPath
{
get
{
return Properties.Settings.Default.SettingsKey;
}
}
Storing the settings values. I chose to use a dictionary. This will get used extensively in a bit. I created a struct as a helper.
/// <summary>
/// In memory storage of the settings values
/// </summary>
private Dictionary<string, SettingStruct> SettingsDictionary { get; set; }
/// <summary>
/// Helper struct.
/// </summary>
internal struct SettingStruct
{
internal string name;
internal string serializeAs;
internal string value;
}
The magic. You must override 2 methods, GetPropertyValues and SetPropertyValues. GetPropertyValues recieves as a parameter what you see in the designer, you have to opportunity to update the values and return a new collection. SetPropertyValues is called when the user saves any changes to the values made at runtime, this is where I update the dictionary and write out the file.
/// <summary>
/// Must override this, this is the bit that matches up the designer properties to the dictionary values
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
/// <returns></returns>
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
{
//load the file
if (!_loaded)
{
_loaded = true;
LoadValuesFromFile();
}
//collection that will be returned.
SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
//iterate thought the properties we get from the designer, checking to see if the setting is in the dictionary
foreach (SettingsProperty setting in collection)
{
SettingsPropertyValue value = new SettingsPropertyValue(setting);
value.IsDirty = false;
//need the type of the value for the strong typing
var t = Type.GetType(setting.PropertyType.FullName);
if (SettingsDictionary.ContainsKey(setting.Name))
{
value.SerializedValue = SettingsDictionary[setting.Name].value;
value.PropertyValue = Convert.ChangeType(SettingsDictionary[setting.Name].value, t);
}
else //use defaults in the case where there are no settings yet
{
value.SerializedValue = setting.DefaultValue;
value.PropertyValue = Convert.ChangeType(setting.DefaultValue, t);
}
values.Add(value);
}
return values;
}
/// <summary>
/// Must override this, this is the bit that does the saving to file. Called when Settings.Save() is called
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
{
//grab the values from the collection parameter and update the values in our dictionary.
foreach (SettingsPropertyValue value in collection)
{
var setting = new SettingStruct()
{
value = (value.PropertyValue == null ? String.Empty : value.PropertyValue.ToString()),
name = value.Name,
serializeAs = value.Property.SerializeAs.ToString()
};
if (!SettingsDictionary.ContainsKey(value.Name))
{
SettingsDictionary.Add(value.Name, setting);
}
else
{
SettingsDictionary[value.Name] = setting;
}
}
//now that our local dictionary is up-to-date, save it to disk.
SaveValuesToFile();
}
Complete solution. So here's the entire class which includes the constructor, Initialize, and helper methods. Feel free to cut/paste slice and dice.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Reflection;
using System.Xml.Linq;
using System.IO;
namespace YourCompany.YourProduct
{
class CustomSettingsProvider : SettingsProvider
{
const string NAME = "name";
const string SERIALIZE_AS = "serializeAs";
const string CONFIG = "configuration";
const string USER_SETTINGS = "userSettings";
const string SETTING = "setting";
/// <summary>
/// Loads the file into memory.
/// </summary>
public CustomSettingsProvider()
{
SettingsDictionary = new Dictionary<string, SettingStruct>();
}
/// <summary>
/// Override.
/// </summary>
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
base.Initialize(ApplicationName, config);
}
/// <summary>
/// Override.
/// </summary>
public override string ApplicationName
{
get
{
return System.Reflection.Assembly.GetExecutingAssembly().ManifestModule.Name;
}
set
{
//do nothing
}
}
/// <summary>
/// Must override this, this is the bit that matches up the designer properties to the dictionary values
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
/// <returns></returns>
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
{
//load the file
if (!_loaded)
{
_loaded = true;
LoadValuesFromFile();
}
//collection that will be returned.
SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
//itterate thought the properties we get from the designer, checking to see if the setting is in the dictionary
foreach (SettingsProperty setting in collection)
{
SettingsPropertyValue value = new SettingsPropertyValue(setting);
value.IsDirty = false;
//need the type of the value for the strong typing
var t = Type.GetType(setting.PropertyType.FullName);
if (SettingsDictionary.ContainsKey(setting.Name))
{
value.SerializedValue = SettingsDictionary[setting.Name].value;
value.PropertyValue = Convert.ChangeType(SettingsDictionary[setting.Name].value, t);
}
else //use defaults in the case where there are no settings yet
{
value.SerializedValue = setting.DefaultValue;
value.PropertyValue = Convert.ChangeType(setting.DefaultValue, t);
}
values.Add(value);
}
return values;
}
/// <summary>
/// Must override this, this is the bit that does the saving to file. Called when Settings.Save() is called
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
{
//grab the values from the collection parameter and update the values in our dictionary.
foreach (SettingsPropertyValue value in collection)
{
var setting = new SettingStruct()
{
value = (value.PropertyValue == null ? String.Empty : value.PropertyValue.ToString()),
name = value.Name,
serializeAs = value.Property.SerializeAs.ToString()
};
if (!SettingsDictionary.ContainsKey(value.Name))
{
SettingsDictionary.Add(value.Name, setting);
}
else
{
SettingsDictionary[value.Name] = setting;
}
}
//now that our local dictionary is up-to-date, save it to disk.
SaveValuesToFile();
}
/// <summary>
/// Loads the values of the file into memory.
/// </summary>
private void LoadValuesFromFile()
{
if (!File.Exists(UserConfigPath))
{
//if the config file is not where it's supposed to be create a new one.
CreateEmptyConfig();
}
//load the xml
var configXml = XDocument.Load(UserConfigPath);
//get all of the <setting name="..." serializeAs="..."> elements.
var settingElements = configXml.Element(CONFIG).Element(USER_SETTINGS).Element(typeof(Properties.Settings).FullName).Elements(SETTING);
//iterate through, adding them to the dictionary, (checking for nulls, xml no likey nulls)
//using "String" as default serializeAs...just in case, no real good reason.
foreach (var element in settingElements)
{
var newSetting = new SettingStruct()
{
name = element.Attribute(NAME) == null ? String.Empty : element.Attribute(NAME).Value,
serializeAs = element.Attribute(SERIALIZE_AS) == null ? "String" : element.Attribute(SERIALIZE_AS).Value,
value = element.Value ?? String.Empty
};
SettingsDictionary.Add(element.Attribute(NAME).Value, newSetting);
}
}
/// <summary>
/// Creates an empty user.config file...looks like the one MS creates.
/// This could be overkill a simple key/value pairing would probably do.
/// </summary>
private void CreateEmptyConfig()
{
var doc = new XDocument();
var declaration = new XDeclaration("1.0", "utf-8", "true");
var config = new XElement(CONFIG);
var userSettings = new XElement(USER_SETTINGS);
var group = new XElement(typeof(Properties.Settings).FullName);
userSettings.Add(group);
config.Add(userSettings);
doc.Add(config);
doc.Declaration = declaration;
doc.Save(UserConfigPath);
}
/// <summary>
/// Saves the in memory dictionary to the user config file
/// </summary>
private void SaveValuesToFile()
{
//load the current xml from the file.
var import = XDocument.Load(UserConfigPath);
//get the settings group (e.g. <Company.Project.Desktop.Settings>)
var settingsSection = import.Element(CONFIG).Element(USER_SETTINGS).Element(typeof(Properties.Settings).FullName);
//iterate though the dictionary, either updating the value or adding the new setting.
foreach (var entry in SettingsDictionary)
{
var setting = settingsSection.Elements().FirstOrDefault(e => e.Attribute(NAME).Value == entry.Key);
if (setting == null) //this can happen if a new setting is added via the .settings designer.
{
var newSetting = new XElement(SETTING);
newSetting.Add(new XAttribute(NAME, entry.Value.name));
newSetting.Add(new XAttribute(SERIALIZE_AS, entry.Value.serializeAs));
newSetting.Value = (entry.Value.value ?? String.Empty);
settingsSection.Add(newSetting);
}
else //update the value if it exists.
{
setting.Value = (entry.Value.value ?? String.Empty);
}
}
import.Save(UserConfigPath);
}
/// <summary>
/// The setting key this is returning must set before the settings are used.
/// e.g. <c>Properties.Settings.Default.SettingsKey = #"C:\temp\user.config";</c>
/// </summary>
private string UserConfigPath
{
get
{
return Properties.Settings.Default.SettingsKey;
}
}
/// <summary>
/// In memory storage of the settings values
/// </summary>
private Dictionary<string, SettingStruct> SettingsDictionary { get; set; }
/// <summary>
/// Helper struct.
/// </summary>
internal struct SettingStruct
{
internal string name;
internal string serializeAs;
internal string value;
}
bool _loaded;
}
}
You would have to implement your own SettingsProvider to customize the path.
See this Client Settings FAQ
Q: Why is the path so obscure? Is there any way to change/customize it?
A: The path construction algorithm has to meet certain rigorous requirements in terms of security, isolation and robustness. While we tried to make the path as easily discoverable as possible by making use of friendly, application supplied strings, it is not possible to keep the path totally simple without running into issues like collisions with other apps, spoofing etc.
The LocalFileSettingsProvider does not provide a way to change the files in which settings are stored. Note that the provider itself doesn't determine the config file locations in the first place - it is the configuration system. If you need to store the settings in a different location for some reason, the recommended way is to write your own SettingsProvider. This is fairly simple to implement and you can find samples in the .NET 2.0 SDK that show how to do this. Keep in mind however that you may run into the same isolation issues mentioned above .
Here is an easier, briefer alternative to creating a custom settings class: change your app's evidence so that the "url" part is a constant rather than being based on the location of executable. To do this, you need to modify the default AppDomain when the program starts. There are two parts: setting app.config to use your AppDomainManager, and creating an AppDomainManager and HostSecurityManager to customize the Url evidence. Sounds complicated but it's much simpler than creating a custom settings class. This only applies to unsigned assemblies. If you have a signed assembly, it's going to use that evidence instead of the Url. But the good news there is your path will always be constant anyway (as long as the signing key doesn't change).
You can copy the code below and just replace the YourAppName bits.
DefaultAppDomainManager.cs:
using System;
using System.Security;
using System.Security.Policy;
namespace YourAppName
{
/// <summary>
/// A least-evil (?) way of customizing the default location of the application's user.config files.
/// </summary>
public class CustomEvidenceHostSecurityManager : HostSecurityManager
{
public override HostSecurityManagerOptions Flags
{
get
{
return HostSecurityManagerOptions.HostAssemblyEvidence;
}
}
public override Evidence ProvideAssemblyEvidence(System.Reflection.Assembly loadedAssembly, Evidence inputEvidence)
{
if (!loadedAssembly.Location.EndsWith("YourAppName.exe"))
return base.ProvideAssemblyEvidence(loadedAssembly, inputEvidence);
// override the full Url used in Evidence to just "YourAppName.exe" so it remains the same no matter where the exe is located
var zoneEvidence = inputEvidence.GetHostEvidence<Zone>();
return new Evidence(new EvidenceBase[] { zoneEvidence, new Url("YourAppName.exe") }, null);
}
}
public class DefaultAppDomainManager : AppDomainManager
{
private CustomEvidenceHostSecurityManager hostSecurityManager;
public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
{
base.InitializeNewDomain(appDomainInfo);
hostSecurityManager = new CustomEvidenceHostSecurityManager();
}
public override HostSecurityManager HostSecurityManager
{
get
{
return hostSecurityManager;
}
}
}
}
app.config excerpt:
<runtime>
<appDomainManagerType value="YourAppName.DefaultAppDomainManager" />
<appDomainManagerAssembly value="DefaultAppDomainManager, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</runtime>
Building on Chucks excellent answer:
Implement a new partial class based on Settings.Designer.cs, so the Settings Designer does not wipe out the attribute when changes are made:
namespace Worker.Properties
{
[System.Configuration.SettingsProvider(
typeof(SettingsProviders.DllFileSettingsProvider<Settings>))]
internal sealed partial class Settings
{
}
}
I created the following class that can be put in an external project. It allows the config to be project.dll.config. By Inheriting and Overriding ConfigPath allows any path you like. I also added support for reading System.Collections.Specialized.StringCollection. This version is for Application settings.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Reflection;
using System.Xml.Linq;
using System.IO;
//http://stackoverflow.com/questions/2265271/custom-path-of-the-user-config
namespace SettingsProviders
{
public class DllFileSettingsProvider<Properties_Settings> : SettingsProvider where Properties_Settings : new()
{
const string NAME = "name";
const string SERIALIZE_AS = "serializeAs";
const string CONFIG = "configuration";
const string APPLICATION_SETTINGS = "applicationSettings";
const string SETTING = "setting";
/// <summary>
/// Loads the file into memory.
/// </summary>
public DllFileSettingsProvider()
{
SettingsDictionary = new Dictionary<string, SettingStruct>();
}
/// <summary>
/// Override.
/// </summary>
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
base.Initialize(ApplicationName, config);
}
/// <summary>
/// Override.
/// </summary>
public override string ApplicationName
{
get
{
return System.Reflection.Assembly.GetExecutingAssembly().ManifestModule.Name;
}
set
{
//do nothing
}
}
/// <summary>
/// Must override this, this is the bit that matches up the designer properties to the dictionary values
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
/// <returns></returns>
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
{
//load the file
if (!_loaded)
{
_loaded = true;
LoadValuesFromFile();
}
//collection that will be returned.
SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
//itterate thought the properties we get from the designer, checking to see if the setting is in the dictionary
foreach (SettingsProperty setting in collection)
{
SettingsPropertyValue value = new SettingsPropertyValue(setting);
value.IsDirty = false;
//need the type of the value for the strong typing
var t = Type.GetType(setting.PropertyType.FullName);
if (setting.PropertyType == typeof(System.Collections.Specialized.StringCollection))
{
var xml = SettingsDictionary[setting.Name].value;
var stringReader = new System.IO.StringReader(xml);
var xmlreader = System.Xml.XmlReader.Create(stringReader);
var ser = new System.Xml.Serialization.XmlSerializer(typeof(System.Collections.Specialized.StringCollection));
var obj = ser.Deserialize(xmlreader);
var col = (System.Collections.Specialized.StringCollection)obj;
value.PropertyValue = col;
}
else if (SettingsDictionary.ContainsKey(setting.Name))
{
value.SerializedValue = SettingsDictionary[setting.Name].value;
value.PropertyValue = Convert.ChangeType(SettingsDictionary[setting.Name].value, t);
}
else //use defaults in the case where there are no settings yet
{
value.SerializedValue = setting.DefaultValue;
value.PropertyValue = Convert.ChangeType(setting.DefaultValue, t);
}
values.Add(value);
}
return values;
}
/// <summary>
/// Must override this, this is the bit that does the saving to file. Called when Settings.Save() is called
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
{
//grab the values from the collection parameter and update the values in our dictionary.
foreach (SettingsPropertyValue value in collection)
{
var setting = new SettingStruct()
{
value = (value.PropertyValue == null ? String.Empty : value.PropertyValue.ToString()),
name = value.Name,
serializeAs = value.Property.SerializeAs.ToString()
};
if (!SettingsDictionary.ContainsKey(value.Name))
{
SettingsDictionary.Add(value.Name, setting);
}
else
{
SettingsDictionary[value.Name] = setting;
}
}
//now that our local dictionary is up-to-date, save it to disk.
SaveValuesToFile();
}
/// <summary>
/// Loads the values of the file into memory.
/// </summary>
private void LoadValuesFromFile()
{
if (!File.Exists(ConfigPath))
{
//if the config file is not where it's supposed to be create a new one.
throw new Exception("Config file not found: " + ConfigPath);
}
//load the xml
var configXml = XDocument.Load(ConfigPath);
//get all of the <setting name="..." serializeAs="..."> elements.
var settingElements = configXml.Element(CONFIG).Element(APPLICATION_SETTINGS).Element(typeof(Properties_Settings).FullName).Elements(SETTING);
//iterate through, adding them to the dictionary, (checking for nulls, xml no likey nulls)
//using "String" as default serializeAs...just in case, no real good reason.
foreach (var element in settingElements)
{
var newSetting = new SettingStruct()
{
name = element.Attribute(NAME) == null ? String.Empty : element.Attribute(NAME).Value,
serializeAs = element.Attribute(SERIALIZE_AS) == null ? "String" : element.Attribute(SERIALIZE_AS).Value ,
value = element.Value ?? String.Empty
};
if (newSetting.serializeAs == "Xml")
{
var e = (XElement)element.Nodes().First();
newSetting.value = e.LastNode.ToString() ?? String.Empty;
};
SettingsDictionary.Add(element.Attribute(NAME).Value, newSetting);
}
}
/// <summary>
/// Creates an empty user.config file...looks like the one MS creates.
/// This could be overkill a simple key/value pairing would probably do.
/// </summary>
private void CreateEmptyConfig()
{
var doc = new XDocument();
var declaration = new XDeclaration("1.0", "utf-8", "true");
var config = new XElement(CONFIG);
var userSettings = new XElement(APPLICATION_SETTINGS);
var group = new XElement(typeof(Properties_Settings).FullName);
userSettings.Add(group);
config.Add(userSettings);
doc.Add(config);
doc.Declaration = declaration;
doc.Save(ConfigPath);
}
/// <summary>
/// Saves the in memory dictionary to the user config file
/// </summary>
private void SaveValuesToFile()
{
//load the current xml from the file.
var import = XDocument.Load(ConfigPath);
//get the settings group (e.g. <Company.Project.Desktop.Settings>)
var settingsSection = import.Element(CONFIG).Element(APPLICATION_SETTINGS).Element(typeof(Properties_Settings).FullName);
//iterate though the dictionary, either updating the value or adding the new setting.
foreach (var entry in SettingsDictionary)
{
var setting = settingsSection.Elements().FirstOrDefault(e => e.Attribute(NAME).Value == entry.Key);
if (setting == null) //this can happen if a new setting is added via the .settings designer.
{
var newSetting = new XElement(SETTING);
newSetting.Add(new XAttribute(NAME, entry.Value.name));
newSetting.Add(new XAttribute(SERIALIZE_AS, entry.Value.serializeAs));
newSetting.Value = (entry.Value.value ?? String.Empty);
settingsSection.Add(newSetting);
}
else //update the value if it exists.
{
setting.Value = (entry.Value.value ?? String.Empty);
}
}
import.Save(ConfigPath);
}
/// <summary>
/// The setting key this is returning must set before the settings are used.
/// e.g. <c>Properties.Settings.Default.SettingsKey = #"C:\temp\user.config";</c>
/// </summary>
public virtual string ConfigPath
{
get
{
var name = new Properties_Settings().GetType().Module.Name + ".config";
if (System.IO.File.Exists(name)==false)
{
System.Diagnostics.Trace.WriteLine("config file NOT found:" + name);
}
System.Diagnostics.Trace.WriteLine("config file found:" + name);
return name;
// return Properties.Settings.Default.SettingsKey;
}
}
/// <summary>
/// In memory storage of the settings values
/// </summary>
internal Dictionary<string, SettingStruct> SettingsDictionary { get; set; }
/// <summary>
/// Helper struct.
/// </summary>
internal struct SettingStruct
{
internal string name;
internal string serializeAs;
internal string value;
}
bool _loaded;
}
}
Make sure dependent projects include the project.dll.config file by add a post-build event:
copy $(SolutionDir)Worker\$(OutDir)Worker.dll.config $(TargetDir) /y

Categories

Resources