Settings Design for ASP.NET Application - c#

First of all, I have been looking online for a Settings Design Pattern but I haven't been able to find a solution that works in my case or only cryptic answers like Use Dependency Injection.
The Problem
I have a large ASP .NET solution which is deployed to multiple production environments. Many decisions in code are taken based on settings which have been implemented in the Web.config file (a few hundreds). At the moment they are all accessed using the NameValueCollection ConfigurationManager.AppSettings. I want to move these settings in the database such that I can create a user interface and modify them more easily afterwards.
I would also prefer for the settings to remain accessible from the Web.config such that if something happens with the database, the application won't break.
Solution Architecture
Basically, the solution consists of the following projects:
UI (this is were the Web.config file resides)
BLL
DAL
Integration Project 1 (the application interracts with other applications. This project uses configuration settings like web addresses, authentication information etc. which at the moment are passed as parameters in the methods - which I personally find very ugly)
Integration Project 2
...
The Code
The Setting class (the database table is practically the same). SettingType is an enum which defines the type of the Setting: Int, String, Bool and I defined for the UI.
public class Setting
{
/// <summary>
/// Gets or sets the identifier.
/// </summary>
/// <value>
/// The identifier.
/// </value>
public int Id { get; set; }
/// <summary>
/// Gets or sets the name of the setting.
/// </summary>
/// <value>
/// The name of the setting.
/// </value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the type of the setting.
/// </summary>
/// <value>
/// The type of the setting.
/// </value>
public SettingType Type { get; set; }
/// <summary>
/// Gets or sets the setting value.
/// </summary>
/// <value>
/// The setting value.
/// </value>
public object Value { get; set; }
/// <summary>
/// Gets or sets the setting description.
/// </summary>
/// <value>
/// The setting description.
/// </value>
public string Description { get; set; }
/// <summary>
/// Gets or sets the setting key.
/// </summary>
/// <value>
/// The setting key.
/// </value>
public string Key { get; set; }
/// <summary>
/// Gets or sets the default value
/// </summary>
/// <value>
/// The default value
/// </value>
public string Default { get; set; }
}
The SettingHelper class (which does everything at the moment, will split functionality in the future):
public static class SettingHelper
{
// Settings collection
private static List<Setting> _settings;
/// <summary>
/// Reloads the settings.
/// </summary>
/// <exception cref="System.Exception"></exception>
public static void LoadSettings()
{
_settings = new List<Setting>();
// Code which loads the settings from the database
}
private static Setting GetSetting(string key)
{
try
{
// if settings are not loaded, we reload them from the database
if (_settings == null || _settings.Count == 0)
LoadSettings();
var value = from Setting setting in _settings
where setting != null && setting.Key == key
select setting;
return value.FirstOrDefault();
}
catch (Exception)
{
return null;
}
}
public static void SetSetting(string key, object newValue, int userID)
{
var currentSetting = GetSetting(key);
if (currentSetting != null && !Convert.ToString(currentSetting.Value).ToUpper().Equals(Convert.ToString(newValue).ToUpper()))
{
// Code which updates the setting value
}
}
// For the UI
public static IEnumerable<Setting> GetAllSettings()
{
if (_settings == null)
LoadSettings();
return _settings;
}
// To change back swiftly to the Web.config in case there are errors in production - will remove in the future
public static bool SettingsFromDataBase
{
get
{
if (HttpContext.Current.Application["SettingsFromDataBase"] == null)
HttpContext.Current.Application["SettingsFromDataBase"] = ConfigurationManager.AppSettings["SettingsFromDataBase"];
bool settingsFromDataBase;
if (bool.TryParse(HttpContext.Current.Application["SettingsFromDataBase"] as string, out settingsFromDataBase))
return settingsFromDataBase;
else return false;
}
}
public static T ObjectToGenericType<T>(object obj)
{
if (obj is T)
{
return (T)obj;
}
else
{
try
{
return (T)Convert.ChangeType(obj, typeof(T));
}
catch (InvalidCastException)
{
return default(T);
}
}
}
public static T GetSetting<T>(string key)
{
if (SettingsFromDataBase)
{
var setting = GetSetting(key);
return setting != null
? ObjectToGenericType<T>(setting.Value)
: ObjectToGenericType<T>(ConfigurationManager.AppSettings[key]);
}
if (HttpContext.Current.Application[key] == null)
HttpContext.Current.Application[key] = ConfigurationManager.AppSettings[key];
return (T)HttpContext.Current.Application[key];
}
// The actual settings which will be used in the other projects
public static string StringSetting
{
get { return GetSetting<string>("StringSetting"); }
}
public static bool BoolSetting
{
get { return GetSetting<bool>("BoolSetting"); }
}
Note: Likely there are improvements to this class using Reflection, but I would prefer to avoid it (apart from that conversion).
Questions
At the moment the SettingHelper resides in the UI project (in order to be able to access the settings from the Web.config). How can I transmit these settings in the BLL or Integration projects?
Also, do you think this implementation with all the settings stored in a static list is the way to go? It feels unoptimal.
Any help or discussion on improvements is appreciated. Thank you.

Related

C# NET Looping over object properties stored in List

At the moment I'm working on funcionality that involves exporting and importing data to Xlsx file. Here's what I want to do: I want to have an attribute I can put above a property like this.
public class MyClass
{
[XlsxColumn("Column 1")]
public string myProperty1 { get; set; }
public int myProperty2 { get; set; }
}
So far I don't have problems, but then I want to "store references" to properties marked with the XlsxColumn attribute. I'm using reflection
to store properties data in List
var propsList = MyClass.GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(XlsxColumn)));
I have a list with all properties marked with XlsxColumn (only myProperty1 in this example).
EDIT: The problem is I don't know how to loop over properties in MyClass, but only properties with XlsxColumn attribute (so all PropertyInfo objects stored in propsList variable), without resorting to reflection with each object saved to Xlsx file.
I'm restricted to .NET 4.0.
Thanks for your time.
MyClass.GetProperties() does not work because you have to get the type of the class to invoke the GetProperties method. Otherwise you are invoking a static method called GetProperties defined in the MyClass class.
var propsList = typeof(MyClass).GetProperties().Where(
prop => prop.IsDefined(typeof(XlsxColumnAttribute), false)).ToList();
If you just want the names (IList<string>):
var propsList = typeof(Excel).GetProperties().Where(
prop => prop.IsDefined(typeof(XlsxColumnAttribute), false))
.Select(prop=> prop.Name)
.ToList();
to use .Where you have to include System.Linq
I must say that I am not sure if this is the solution you are looking for. Because I could not quite make out what your question is. Well I have tried to provide an answer as much as I could figure out.
I went for a static class for CachingPropetyProvider but you can go for an instance class and use a dependency injection library and use it as a Singleton too. Moreover, I have written extensive comments so It is as self explanatory as possible.
Let us define MyClass. I also deliberately changed it a little bit.
public class MyClass
{
[XlsxColumn("Column 1")]
public string MyProperty1 { get; set; }
[XlsxColumn("Column 2")]
public int MyProperty2 { get; set; }
}
I also defined a MetaInfo class to hold the cached information.
public class MetaInfo {
/// <summary>
/// Immutable class for holding PropertyInfo and XlsxColumn info.
/// </summary>
/// <param name="info">PropertyInfo</param>
/// <param name="attr">XlsxColumn</param>
public MetaInfo(PropertyInfo info, XlsxColumn attr) {
PropertyInfo = info;
Attribute = attr;
}
/// <summary>
/// PropertyInfo. You may want to access the value inside the property.
/// </summary>
public PropertyInfo PropertyInfo { get; }
/// <summary>
/// Attribute. You may want to access information hold inside the attribute.
/// </summary>
public XlsxColumn Attribute { get; }
}
And lastly the main guy. This guy is responsible for providing all the data about classes
public class CachingPropProvider {
/// <summary>
/// Holds the meta information for each type.
/// </summary>
private static readonly ConcurrentDictionary<Type, List<MetaInfo>> TypeCache;
/// <summary>
/// Static constructor is guaranteed to run only once.
/// </summary>
static CachingPropProvider() {
//Initialize the cache.
TypeCache = new ConcurrentDictionary<Type, List<MetaInfo>>();
}
/// <summary>
/// Gets the MetaInfo for the given type. Since We use ConcurrentDictionary it is thread safe.
/// </summary>
/// <typeparam name="T">Type parameter</typeparam>
public static IEnumerable<MetaInfo> GetCachedStuff<T>() {
//If Type exists in the TypeCache, return the cached value
return TypeCache.GetOrAdd(typeof(T),Factory);
}
/// <summary>
/// Factory method to use to extract MetaInfo when Cache is not hit.
/// </summary>
/// <param name="type">Type to extract info from</param>
/// <returns>A list of MetaInfo. An empty List, if no property has XlsxColumn attrbiute</returns>
private static List<MetaInfo> Factory(Type #type) {
//If Type does not exist in the TypeCahce runs Extractor
//Method to extract metainfo for the given type
return #type.GetProperties().Aggregate(new List<MetaInfo>(), Extractor);
}
/// <summary>
/// Extracts MetaInfo from the given property info then saves it into the list.
/// </summary>
/// <param name="seedList">List to save metainfo into</param>
/// <param name="propertyInfo">PropertyInfo to try to extract info from</param>
/// <returns>List of MetaInfo</returns>
private static List<MetaInfo> Extractor(List<MetaInfo> seedList,PropertyInfo propertyInfo) {
//Gets Attribute
var customattribute = propertyInfo.GetCustomAttribute<XlsxColumn>();
//If custom attribute is not null, it means it is defined
if (customattribute != null)
{
//Extract then add it into seed list
seedList.Add(new MetaInfo(propertyInfo, customattribute));
}
//Return :)
return seedList;
}
}
Finally let us see how to use the solution. It is pretty straightforward actually.
//Has 2 values inside
var info = CachingPropProvider.GetCachedStuff<MyClass>();

how to set private structure fields in a public property?

I have a structure:
public struct ServiceDescription
{
string serviceDescriptionText;
string serviceLogoRef;
/// <summary>
/// The position to print the service on the label.
/// </summary>
int servicePosition;
/// <summary>
/// Constructor
/// </summary>
/// <param name="serviceDescriptionText"></param>
/// <param name="serviceLogoRef"></param>
/// <param name="servicePosition"></param>
public ServiceDescription(string serviceDescriptionText, string serviceLogoRef,
int servicePosition)
{
this.serviceDescriptionText = serviceDescriptionText;
this.serviceLogoRef = serviceLogoRef;
this.servicePosition = servicePosition;
}
}
and a property:
public string pServiceDescription
{
get
{
return p_sServiceDescription;
}
// set private structure field 1
// set private structure field 2
// etc...
}
How do I set each of the private fields of the structure in the setters of my property?
It is usually a very bad idea to have mutable structs; mutable value-type semantics is not what people usually expect. You can, however, simply add a set that works like any other regular setter. It just isn't a very good idea:
public string Text
{
get { return text; }
set { text = value; } // this is a really bad idea on a struct
}
If it was me, that would be an immutable struct with private readonly fields and a constructor that sets all of them (and get-only properties), or a class - probably with automatically implemented properties, i.e.
public class ServiceDescription {
public string Text {get;set;}
//...
}

How to serialize a dictionary to an XML file?

I'm trying to maintain a dictionary of configurations.
Here is my abstract class.
[Serializable]
public abstract class Configuration
{
}
And here is a concrete class (for the moment, I just have only this class).
[Serializable]
public class BinaryProblemConfiguration : Configuration
{
[XmlAttribute]
public decimal MinValue { get; set; }
[XmlAttribute]
public decimal MaxValue { get; set; }
}
I've got a class which contains a Dictionary of configuration levels.
The first parameter is the name of the configuration. When name="" means default configuration.
Level means the difficulty. There are three levels: Easy, Medium and Hard.
And the third one is the configuration.
/// <summary>
/// The abstract level configuration allows descendent classes to configure themselves
/// </summary>
public abstract class LevelConfiguration
{
private Dictionary<string, Dictionary<Levels, Configuration>> _configurableLevels = new Dictionary<string, Dictionary<Levels, Configuration>>();
/// <summary>
/// Adds a configurable level.
/// </summary>
/// <param name="level">The level to add.</param>
/// <param name="problemConfiguration">The problem configuration.</param>
protected void AddConfigurableLevel(string name, Levels level, Configuration problemConfiguration)
{
if (!_configurableLevels.ContainsKey(name))
{
_configurableLevels.Add(name, new Dictionary<Levels, Configuration>());
}
_configurableLevels[name].Add(level, problemConfiguration);
}
/// <summary>
/// Returns all the configurable levels.
/// </summary>
/// <param name="level"></param>
protected void RemoveConfigurableLevel(string name, Levels level)
{
_configurableLevels[name].Remove(level);
}
/// <summary>
/// Returns all the configurable names.
/// </summary>
/// <returns></returns>
public IEnumerable<string> GetConfigurationNames()
{
return _configurableLevels.Keys;
}
/// <summary>
/// Returns all the configurable levels.
/// </summary>
/// <returns></returns>
public IEnumerable<Levels> GetConfigurationLevels(string name)
{
return _configurableLevels[name].Keys;
}
/// <summary>
/// Gets the problem configuration for the specified level
/// </summary>
/// <param name="level">The level.</param>
/// <returns></returns>
public Configuration GetProblemConfiguration(string name, Levels level)
{
return _configurableLevels[name][level];
}
}
This is the class which create some configurations. I'm creating three default configs and two customs.
public class AdditionLevelConfiguration : LevelConfiguration
{
public AdditionLevelConfiguration()
{
AddConfigurableLevel("", Levels.Easy, GetEasyLevelConfiguration());
AddConfigurableLevel("", Levels.Medium, GetMediumLevelConfiguration());
AddConfigurableLevel("", Levels.Hard, GetHardLevelConfiguration());
AddConfigurableLevel("config2", Levels.Easy, GetEasyLevelConfiguration());
AddConfigurableLevel("config2", Levels.Medium, GetMediumLevelConfiguration());
var configs = this.GetProblemConfiguration("config2", Levels.Medium);
var configs2 = this.GetProblemConfiguration("", Levels.Easy);
}
protected Configuration GetHardLevelConfiguration()
{
return new BinaryProblemConfiguration
{
MinValue = 100,
MaxValue = 1000,
};
}
protected Configuration GetMediumLevelConfiguration()
{
return new BinaryProblemConfiguration
{
MinValue = 10,
MaxValue = 100,
};
}
protected Configuration GetEasyLevelConfiguration()
{
return new BinaryProblemConfiguration
{
MinValue = 1,
MaxValue = 10,
};
}
}
I plan to write these configurations in a XML file. I was thinking of serialize them, but it throws me an error. What should I do?
Another option you may consider is using a DataContractSerializer. I was able to serialize your dictionary of dictionaries this way.
Things to keep in mind if you go this route:
You would have to add different attributes. Specifically, you need DataContract on the type and DataMember on the properties.
You need to ensure all properties have setters, even if the setters ultimately are private.
When creating your DataContractSerializer, you need to ensure it is aware of all your types. Giving it the type of your dictionary takes care of string, Levels, and Configuration, but not BinaryProblemConfiguration. Additional type info can be provided via additional constructor overloads.
Example:
var dict = new Dictionary<string,Dictionary<Levels,Configuration>> ();
var wtr = XmlWriter.Create (Console.Out);
var dcSerializer = new DataContractSerializer (dict.GetType (), new [] {typeof (BinaryProblemConfiguration)});
dcSerializer.WriteObject (wtr, dict);
If you do the above, you could also switch to DataContractJsonSerializer later for a more compact JSON format if you prefer (assuming XML is not a hard requirement, of course).
From what i know classes that implement IDictionary cannot be Xml serialized.
Try this http://weblogs.asp.net/pwelter34/archive/2006/05/03/444961.aspx, it worked for me.

MemoryCache with regions support?

I need to add cache functionality and found a new shiny class called MemoryCache. However, I find MemoryCache a little bit crippled as it is (I'm in need of regions functionality). Among other things I need to add something like ClearAll(region). Authors made a great effort to keep this class without regions support, code like:
if (regionName != null)
{
throw new NotSupportedException(R.RegionName_not_supported);
}
flies in almost every method.
I don't see an easy way to override this behaviour. The only way to add region support that I can think of is to add a new class as a wrapper of MemoryCache rather then as a class that inherits from MemoryCache. Then in this new class create a Dictionary and let each method "buffer" region calls. Sounds nasty and wrong, but eventually...
Do you know of better ways to add regions to MemoryCache?
I know it is a long time since you asked this question, so this is not really an answer to you, but rather an addition for future readers.
I was also surprised to find that the standard implementation of MemoryCache does NOT support regions. It would have been so easy to provide right away. I therefore decided to wrap the MemoryCache in my own simple class to provide the functionality I often need.
I enclose my code it here to save time for others having the same need!
/// <summary>
/// =================================================================================================================
/// This is a static encapsulation of the Framework provided MemoryCache to make it easier to use.
/// - Keys can be of any type, not just strings.
/// - A typed Get method is provided for the common case where type of retrieved item actually is known.
/// - Exists method is provided.
/// - Except for the Set method with custom policy, some specific Set methods are also provided for convenience.
/// - One SetAbsolute method with remove callback is provided as an example.
/// The Set method can also be used for custom remove/update monitoring.
/// - Domain (or "region") functionality missing in default MemoryCache is provided.
/// This is very useful when adding items with identical keys but belonging to different domains.
/// Example: "Customer" with Id=1, and "Product" with Id=1
/// =================================================================================================================
/// </summary>
public static class MyCache
{
private const string KeySeparator = "_";
private const string DefaultDomain = "DefaultDomain";
private static MemoryCache Cache
{
get { return MemoryCache.Default; }
}
// -----------------------------------------------------------------------------------------------------------------------------
// The default instance of the MemoryCache is used.
// Memory usage can be configured in standard config file.
// -----------------------------------------------------------------------------------------------------------------------------
// cacheMemoryLimitMegabytes: The amount of maximum memory size to be used. Specified in megabytes.
// The default is zero, which indicates that the MemoryCache instance manages its own memory
// based on the amount of memory that is installed on the computer.
// physicalMemoryPercentage: The percentage of physical memory that the cache can use. It is specified as an integer value from 1 to 100.
// The default is zero, which indicates that the MemoryCache instance manages its own memory
// based on the amount of memory that is installed on the computer.
// pollingInterval: The time interval after which the cache implementation compares the current memory load with the
// absolute and percentage-based memory limits that are set for the cache instance.
// The default is two minutes.
// -----------------------------------------------------------------------------------------------------------------------------
// <configuration>
// <system.runtime.caching>
// <memoryCache>
// <namedCaches>
// <add name="default" cacheMemoryLimitMegabytes="0" physicalMemoryPercentage="0" pollingInterval="00:02:00" />
// </namedCaches>
// </memoryCache>
// </system.runtime.caching>
// </configuration>
// -----------------------------------------------------------------------------------------------------------------------------
/// <summary>
/// Store an object and let it stay in cache until manually removed.
/// </summary>
public static void SetPermanent(string key, object data, string domain = null)
{
CacheItemPolicy policy = new CacheItemPolicy { };
Set(key, data, policy, domain);
}
/// <summary>
/// Store an object and let it stay in cache x minutes from write.
/// </summary>
public static void SetAbsolute(string key, object data, double minutes, string domain = null)
{
CacheItemPolicy policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(minutes) };
Set(key, data, policy, domain);
}
/// <summary>
/// Store an object and let it stay in cache x minutes from write.
/// callback is a method to be triggered when item is removed
/// </summary>
public static void SetAbsolute(string key, object data, double minutes, CacheEntryRemovedCallback callback, string domain = null)
{
CacheItemPolicy policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(minutes), RemovedCallback = callback };
Set(key, data, policy, domain);
}
/// <summary>
/// Store an object and let it stay in cache x minutes from last write or read.
/// </summary>
public static void SetSliding(object key, object data, double minutes, string domain = null)
{
CacheItemPolicy policy = new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(minutes) };
Set(key, data, policy, domain);
}
/// <summary>
/// Store an item and let it stay in cache according to specified policy.
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="data">Object to store</param>
/// <param name="policy">CacheItemPolicy</param>
/// <param name="domain">NULL will fallback to default domain</param>
public static void Set(object key, object data, CacheItemPolicy policy, string domain = null)
{
Cache.Add(CombinedKey(key, domain), data, policy);
}
/// <summary>
/// Get typed item from cache.
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="domain">NULL will fallback to default domain</param>
public static T Get<T>(object key, string domain = null)
{
return (T)Get(key, domain);
}
/// <summary>
/// Get item from cache.
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="domain">NULL will fallback to default domain</param>
public static object Get(object key, string domain = null)
{
return Cache.Get(CombinedKey(key, domain));
}
/// <summary>
/// Check if item exists in cache.
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="domain">NULL will fallback to default domain</param>
public static bool Exists(object key, string domain = null)
{
return Cache[CombinedKey(key, domain)] != null;
}
/// <summary>
/// Remove item from cache.
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="domain">NULL will fallback to default domain</param>
public static void Remove(object key, string domain = null)
{
Cache.Remove(CombinedKey(key, domain));
}
#region Support Methods
/// <summary>
/// Parse domain from combinedKey.
/// This method is exposed publicly because it can be useful in callback methods.
/// The key property of the callback argument will in our case be the combinedKey.
/// To be interpreted, it needs to be split into domain and key with these parse methods.
/// </summary>
public static string ParseDomain(string combinedKey)
{
return combinedKey.Substring(0, combinedKey.IndexOf(KeySeparator));
}
/// <summary>
/// Parse key from combinedKey.
/// This method is exposed publicly because it can be useful in callback methods.
/// The key property of the callback argument will in our case be the combinedKey.
/// To be interpreted, it needs to be split into domain and key with these parse methods.
/// </summary>
public static string ParseKey(string combinedKey)
{
return combinedKey.Substring(combinedKey.IndexOf(KeySeparator) + KeySeparator.Length);
}
/// <summary>
/// Create a combined key from given values.
/// The combined key is used when storing and retrieving from the inner MemoryCache instance.
/// Example: Product_76
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="domain">NULL will fallback to default domain</param>
private static string CombinedKey(object key, string domain)
{
return string.Format("{0}{1}{2}", string.IsNullOrEmpty(domain) ? DefaultDomain : domain, KeySeparator, key);
}
#endregion
}
You can create more than one just one MemoryCache instance, one for each partition of your data.
http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache.aspx :
you can create multiple instances of the MemoryCache class for use in the same application and in the same AppDomain instance
I just recently came across this problem. I know this is an old question but maybe this might be useful for some folks. Here is my iteration of the solution by Thomas F. Abraham
namespace CLRTest
{
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.Caching;
class Program
{
static void Main(string[] args)
{
CacheTester.TestCache();
}
}
public class SignaledChangeEventArgs : EventArgs
{
public string Name { get; private set; }
public SignaledChangeEventArgs(string name = null) { this.Name = name; }
}
/// <summary>
/// Cache change monitor that allows an app to fire a change notification
/// to all associated cache items.
/// </summary>
public class SignaledChangeMonitor : ChangeMonitor
{
// Shared across all SignaledChangeMonitors in the AppDomain
private static ConcurrentDictionary<string, EventHandler<SignaledChangeEventArgs>> ListenerLookup =
new ConcurrentDictionary<string, EventHandler<SignaledChangeEventArgs>>();
private string _name;
private string _key;
private string _uniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
public override string UniqueId
{
get { return _uniqueId; }
}
public SignaledChangeMonitor(string key, string name)
{
_key = key;
_name = name;
// Register instance with the shared event
ListenerLookup[_uniqueId] = OnSignalRaised;
base.InitializationComplete();
}
public static void Signal(string name = null)
{
// Raise shared event to notify all subscribers
foreach (var subscriber in ListenerLookup.ToList())
{
subscriber.Value?.Invoke(null, new SignaledChangeEventArgs(name));
}
}
protected override void Dispose(bool disposing)
{
// Set delegate to null so it can't be accidentally called in Signal() while being disposed
ListenerLookup[_uniqueId] = null;
EventHandler<SignaledChangeEventArgs> outValue = null;
ListenerLookup.TryRemove(_uniqueId, out outValue);
}
private void OnSignalRaised(object sender, SignaledChangeEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.Name) || string.Compare(e.Name, _name, true) == 0)
{
// Cache objects are obligated to remove entry upon change notification.
base.OnChanged(null);
}
}
}
public static class CacheTester
{
private static Stopwatch _timer = new Stopwatch();
public static void TestCache()
{
MemoryCache cache = MemoryCache.Default;
int size = (int)1e6;
Start();
for (int idx = 0; idx < size; idx++)
{
cache.Add(idx.ToString(), "Value" + idx.ToString(), GetPolicy(idx, cache));
}
long prevCnt = cache.GetCount();
Stop($"Added {prevCnt} items");
Start();
SignaledChangeMonitor.Signal("NamedData");
Stop($"Removed {prevCnt - cache.GetCount()} entries");
prevCnt = cache.GetCount();
Start();
SignaledChangeMonitor.Signal();
Stop($"Removed {prevCnt - cache.GetCount()} entries");
}
private static CacheItemPolicy GetPolicy(int idx, MemoryCache cache)
{
string name = (idx % 10 == 0) ? "NamedData" : null;
CacheItemPolicy cip = new CacheItemPolicy();
cip.AbsoluteExpiration = System.DateTimeOffset.UtcNow.AddHours(1);
var monitor = new SignaledChangeMonitor(idx.ToString(), name);
cip.ChangeMonitors.Add(monitor);
return cip;
}
private static void Start()
{
_timer.Start();
}
private static void Stop(string msg = null)
{
_timer.Stop();
Console.WriteLine($"{msg} | {_timer.Elapsed.TotalSeconds} sec");
_timer.Reset();
}
}
}
His solution involved using an event to keep track of ChangeMonitors. But the dispose method was working slow when the number of entries were more than 10k. My guess is that this code SignaledChangeMonitor.Signaled -= OnSignalRaised removes a delegate from invocation list by doing a linear search. So when you remove a lot of entries it becomes slow. I decided to use ConcurrentDictionary instead of an event. In hope that dispose becomes faster. I ran some basic performance tests and here are the results:
Added 10000 items | 0.027697 sec
Removed 1000 entries | 0.0040669 sec
Removed 9000 entries | 0.0105687 sec
Added 100000 items | 0.5065736 sec
Removed 10000 entries | 0.0338991 sec
Removed 90000 entries | 0.1418357 sec
Added 1000000 items | 6.5994546 sec
Removed 100000 entries | 0.4176233 sec
Removed 900000 entries | 1.2514225 sec
I am not sure if my code does not have some critical flaws. I would like to know if that is the case.
Another approach is to implement a wrapper around MemoryCache that implements regions by composing the key and region name e.g.
public interface ICache
{
...
object Get(string key, string regionName = null);
...
}
public class MyCache : ICache
{
private readonly MemoryCache cache
public MyCache(MemoryCache cache)
{
this.cache = cache.
}
...
public object Get(string key, string regionName = null)
{
var regionKey = RegionKey(key, regionName);
return cache.Get(regionKey);
}
private string RegionKey(string key, string regionName)
{
// NB Implements region as a suffix, for prefix, swap order in the format
return string.IsNullOrEmpty(regionName) ? key : string.Format("{0}{1}{2}", key, "::", regionName);
}
...
}
It's not perfect but it works for most use cases.
I've implemented this and it's available as a NuGet package: Meerkat.Caching

My C# COM Library Isn't Working Right; Can't Instantiate The Class and a Method Appears As a Property

I'm writing a COM DLL in C# to handle the import and export of X.12 format documents, so I would be able to use it in an Access database and a custom program for handling EDI with my company. I've gotten a DLL to compile but with disappointing results, and I'm wondering if I'm missing something; COM "from scratch" is new ground to me (I've made a ribbon for Excel before but a wizard handled all of that).
I've read this article on MSDN and came across this question here to get my DLL and TLB to compile and register. This is the skeleton of my X12Segment class and the interface for COM visibility:
using System;
using System.Collections;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
namespace X12
{
[Guid("28A76274-05EE-45B2-A8EF-ADD5A5B351DE"),
ComVisible(true)]
public interface IX12Segment
{
[DispId(1)]
string SegmentType { get; set; }
[DispId(2)]
string[] Fields { get; set; }
[DispId(3)]
char FieldDelimiter { get; set; }
[DispId(4)]
char SegmentDelimiter { get; set; }
[DispId(5)]
string ToString(char sep, char eol);
[DispId(6)]
string ToString();
[DispId(7)]
Type GetFieldEnum();
}
[Guid("B321599A-E5EC-4510-A021-E9A8B4D6293E"),
ClassInterface(ClassInterfaceType.None),
ComVisible(true)]
public class X12Segment : IX12Segment
{
private string _type;
protected ArrayList _fields;
protected short _minFields = -1;
protected short _maxFields = -1;
protected short[][] _fieldSizes = { new short[] { -1, -1 } };
protected char _sep;
protected char _eol;
public enum Field { }
/// <summary>
/// Creates a new X.12 segment of the supplied type,
/// optionally with a supplied number of fields or
/// values.
/// </summary>
/// <param name="segType">The type of segment (eg "ISA", "GS")</param>
/// <param name="fields">Each string is a field
/// within the segment</param>
public X12Segment(string segType, params string[] fields)
: this(segType)
{
//Do cool stuff
}
/// <summary>
/// Creates a new X.12 segment from a string.
/// </summary>
/// <param name="segType">The type of segment (eg "ISA", "GS")</param>
/// <param name="segment">The string to parse</param>
public X12Segment(string segType, string segment)
: this(segType)
{
//Do cool stuff
}
/// <summary>
/// Creates a new X.12 segment of the supplied type,
/// optionally with a supplied number of fields or
/// values.
/// </summary>
/// <param name="segType">The type of segment (eg "ISA", "GS")</param>
/// <param name="fieldCount">The number of fields
/// in this segment</param>
public X12Segment(string segType, int fieldCount) : this(segType)
{
//Do cool stuff
}
/// <summary>
/// Creates a new X.12 segment of the supplied type,
/// optionally with a supplied number of fields or
/// values.
/// </summary>
/// <param name="segType">The type of segment (eg "ISA", "GS")</param>
public X12Segment(string segType) : this()
{
//Do cool stuff
}
public X12Segment()
{
//Do cool stuff
}
/// <summary>
/// Gets or sets the segment type.
/// </summary>
public string SegmentType
{
get;
set;
}
/// <summary>
/// Gets or sets all of the fields in the segment,
/// in the form of an array of strings.
/// </summary>
public string[] Fields
{
get;
set;
}
/// <summary>
/// Gets or sets the character used to seperate fields in the segment.
/// </summary>
public char FieldDelimiter
{
get;
set;
}
/// <summary>
/// Gets or sets the character denoting the end of the segment.
/// </summary>
public char SegmentDelimiter
{
get;
set;
}
/// <summary>
/// Generates an X.12 formatted segment.
/// </summary>
/// <param name="sep">The field delimiter to use.</param>
/// <param name="eol">The segment delimiter to use.</param>
/// <returns>An X.12 formatted string.</returns>
public string ToString(char sep, char eol)
{
//Do cool stuff
}
/// <summary>
/// Generates an X.12 formatted segment.
/// </summary>
/// <returns>An X.12 formatted string.</returns>
public override string ToString()
{
//Do cool stuff
}
/// <summary>
/// Returns the Type associated with the Field enumeration of this object.
/// </summary>
/// <returns>A System.Type of this object's Field enumeration.</returns>
public virtual Type GetFieldEnum()
{
//Do cool stuff
}
}
}
Now, when I open up VBA and add the reference, the class shows up in IntelliSense. However, when I Dim a variable with the X12Segment type, then put in the dot operator, the IntelliSense window that pops up shows me that ToString() is a property, not a method. Also, ToString's overload shows up as ToString_2. When I attempt Set seg = New X12Segment, VBA tells me that it's an invalid use of the New keyword.
What am I missing here?
Update
I've revised my code as per the comments and answer below, and I have solutions for New not working and my ToString overload appearing funky in IntelliSense. A new problem arises, though; trying to access Fields gives me errors. seg.Fields = someStringArray gets me an error saying "Function or interface marked as restricted, or the function uses an Automation type not supported in Visual Basic".
A [ComVisible] class must have a parameterless constructor. COM doesn't have a mechanism to pass arguments to a constructor. You provided only constructors that take arguments which is why VBA doesn't let you use the New keyword.
COM also doesn't support method overloading. You need to give the methods distinct names. If you don't then the type library exporter will take care of it automatically. Thus ToString_2().

Categories