I have strange behaviour that my code at specific place is not stepping in specific method. There is no error, no nothing. It is just reaching the line without stepping into it. I was debugging and stepping into each tep to found that issue. I have no idea what's going on, that's first time i face such an issue. Below find my code and at the end explained exactly where it happens.
static class Program
{
private static UnityContainer container;
[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Bootstrap();
Application.Run(container.Resolve<FrmLogin>());
}
private static void Bootstrap()
{
container = new UnityContainer();
container.RegisterType<IRepositoryDal<User>, UserRepositoryDal>();
container.RegisterType<IRepositoryDal<Order>, OrderRepositoryDal>();
container.RegisterType<IDbManager, DbManager>(new InjectionConstructor("sqlserver"));
container.RegisterType<IGenericBal<User>, UserBal>();
container.RegisterType<IGenericBal<Order>, OrderBal>();
}
}
public partial class FrmLogin : Form
{
private readonly IGenericBal<User> _userBal;
public FrmLogin(IGenericBal<User> userBal)
{
InitializeComponent();
_userBal = userBal;
}
private void btnSearch_Click(object sender, EventArgs e)
{
try
{
var a = _userBal.SearchByName("John");
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
}
public class UserBal : IGenericBal<User>
{
private readonly IRepositoryDal<User> _userRepositoryDal;
public UserBal(IRepositoryDal<User> userRepositoryDal)
{
_userRepositoryDal = userRepositoryDal ?? throw new ArgumentNullException(nameof(userRepositoryDal));
}
public IEnumerable<User> SearchByName(string name)
{
return _userRepositoryDal.SearchByName(name);
}
}
public interface IGenericBal<out T> where T : IEntity
{
IEnumerable<T> SearchByName(string name);
}
public class UserRepositoryDal: IRepositoryDal<User>
{
private readonly IDbManager _dbManager;
public UserRepositoryDal(IDbManager dbManager)
{
_dbManager = dbManager;
}
public IEnumerable<User> SearchByName(string username)
{
var parameters = new List<IDbDataParameter>
{
_dbManager.CreateParameter("#Name", 50, username, DbType.String),
};
username = "JUSTyou";
var userDataTable = _dbManager.GetDataTable("SELECT * FROM T_Marke WHERE Name=#Name", CommandType.Text, parameters.ToArray());
foreach (DataRow dr in userDataTable.Rows)
{
var user = new User
{
Id = int.Parse(dr["Id"].ToString()),
Firstname = dr["Name"].ToString(),
};
yield return user;
}
}
}
public interface IRepositoryDal<T> where T : IEntity
{
IEnumerable<T> SearchByName(string username);
T SearchById(string id);
void Update(T entity);
void Remove(T entity);
void Add(T entity);
}
What happens here is:
When i start to debug using breakpoints i start to click button which raises btnSearch_Click handler you can find in my code. When it happens it goes to: var a = _userBal.SearchByName("John"); then to UserBal's code SearchByName method. When it reaches: return _userRepositoryDal.SearchByName(name); it's not going into in this case UserRepositoryDal's SerachByName method. It's just highlight this line of code and going next but not inside. No error, no nothing... Why it happens?
This is called "Lazy evaluation": https://blogs.msdn.microsoft.com/pedram/2007/06/02/lazy-evaluation-in-c/
In brief, you use yield return to return the method's results, which means that the code does not get evaluated immediately, but the actual method execution gets postponed up to until you actually use some results of the evaluation.
Update:
If you want to evaluate your code immediately, you need to use it somehow. The simplest way would be to return the whole result set to create a new array or list from it. You can do it, for instance, by replacing:
return _userRepositoryDal.SearchByName(name);
with:
return _userRepositoryDal.SearchByName(name).ToList();
While this might be good for debugging, it will also remove the performance gains you obtain by using lazy evaluation.
This bit of code is an Lazy Enumeration:
public IEnumerable<User> SearchByName(string username)
{
var parameters = new List<IDbDataParameter>
{
_dbManager.CreateParameter("#Name", 50, username, DbType.String),
};
username = "JUSTyou";
var userDataTable = _dbManager.GetDataTable("SELECT * FROM T_Marke WHERE Name=#Name", CommandType.Text, parameters.ToArray());
foreach (DataRow dr in userDataTable.Rows)
{
var user = new User
{
Id = int.Parse(dr["Id"].ToString()),
Firstname = dr["Name"].ToString(),
};
yield return user;
}
}
By using yield return user; your telling .Net to only run this code when it's enumerated. At no point are you accessing the result of SearchByName. So it won't step into it:
public IEnumerable<User> SearchByName(string name)
{
//this doesn't access the result
return _userRepositoryDal.SearchByName(name);
//this would
//return _userRepositoryDal.SearchByName(name).ToList();
}
The easiest way to "fix" this is to remove the enumeration as I don't think this is what you want:
public IEnumerable<User> SearchByName(string username)
{
List<User> response = new List<User>();
var parameters = new List<IDbDataParameter>
{
_dbManager.CreateParameter("#Name", 50, username, DbType.String),
};
username = "JUSTyou";
var userDataTable = _dbManager.GetDataTable("SELECT * FROM T_Marke WHERE Name=#Name", CommandType.Text, parameters.ToArray());
foreach (DataRow dr in userDataTable.Rows)
{
var user = new User
{
Id = int.Parse(dr["Id"].ToString()),
Firstname = dr["Name"].ToString(),
};
//Add to a collection
response.Add(user);
}
//return result
return response;
}
Related
I'm implementing the MailChimp.NET wrapper in both synchronous and asynchronous ways and calls are going through without a problem, BUT results tend to get lost in the synchronous methods. In other words, if I send 100 members to be added (by batches of 10 due to the simultaneous connections limit of the MailChimp API), all 100 will indeed be visible in my MC audience but I'll loose from 5 to 25% of the results on code side. Here's the concerned bit of my implementation :
public class MailChimpClient : IDisposable
{
private MailChimpManager _mcm;
private string _apiKey;
private bool _isDisposed;
private ConcurrentQueue<MailChimpMember> _updatedMembersQueue;
private ConcurrentQueue<MailChimpBaseException> _exceptionsQueue;
private const int BatchSize = 10;
private const int TaskDelay = 100;
private ConcurrentQueue<MailChimpMember> UpdatedMembersQueue
{
get { return _updatedMembersQueue = _updatedMembersQueue ?? new ConcurrentQueue<MailChimpMember>(); }
set { _updatedMembersQueue = value; }
}
private ConcurrentQueue<MailChimpBaseException> ExceptionsQueue
{
get { return _exceptionsQueue = _exceptionsQueue ?? new ConcurrentQueue<MailChimpBaseException>(); }
set { _exceptionsQueue = value; }
}
public MailChimpClient(string apiKey)
{
_apiKey = apiKey;
_mcm = new MailChimpManager(apiKey);
}
private async Task AddOrUpdateMember(MailChimpMember member, string listId)
{
try
{
var model = member.ToApiMember();
model = await _mcm.Members.AddOrUpdateAsync(listId, model);
UpdatedMembersQueue.Enqueue(new MailChimpMember(model));
await Task.Delay(TaskDelay);
}
catch (Exception ex)
{
var mccex = new MailChimpClientException($"Error adding/updating member \"{(member != null ? member.MailAddress.ToString() : "NULL")}\" to list with ID \"{listId}\".", ex);
ExceptionsQueue.Enqueue(mccex);
}
}
private MailChimpClientResult AddOrUpdateMemberRange(IEnumerable<MailChimpMember> members, string listId)
{
var batches = members.GetBatches(BatchSize);
var result = new MailChimpClientResult();
var i = 0;
foreach (var batch in batches)
{
AddOrUpdateMemberBatch(batch, listId);
i++;
FlushQueues(ref result);
}
return result;
}
private void AddOrUpdateMemberBatch(MailChimpMember[] batch, string listId)
{
Task.WaitAll(batch.Select(async b => await AddOrUpdateMember(b, listId)).ToArray(), -1);
}
private void FlushQueues(ref MailChimpClientResult result)
{
result.UpdatedMembers.FlushQueue(UpdatedMembersQueue);
result.Exceptions.FlushQueue(ExceptionsQueue);
}
public MailChimpClientResult AddOrUpdate(MailChimpMember member, string listId)
{
return AddOrUpdateMemberRange(new MailChimpMember[] { member }, listId);
}
public MailChimpClientResult AddOrUpdate(IEnumerable<MailChimpMember> members, string listId)
{
return AddOrUpdateMemberRange(members, listId);
}
}
public static class CollectionExtensions
{
public static T[][] GetBatches<T>(this IEnumerable<T> items, int batchSize)
{
var result = new List<T[]>();
var batch = new List<T>();
foreach (var t in items)
{
if (batch.Count == batchSize)
{
result.Add(batch.ToArray());
batch.Clear();
}
batch.Add(t);
}
result.Add(batch.ToArray());
batch.Clear();
return result.ToArray();
}
public static void FlushQueue<T>(this IList<T> list, ConcurrentQueue<T> queue)
{
T item;
while (queue.TryDequeue(out item))
list.Add(item);
}
}
MailChimpMember being a public copy of the MailChimp.NET Member. The problem seems to happen in the batch processing method, the Task.WaitAll(...) instruction firing its completion event before all calls are complete, therefore not all results are queued. I tried delaying the execution of each individual treatment with Task.Delay() but with little to no result.
Does anyone have an idea what is failing in my implementation ?
In my case it's a web API project in Visual Studio. When I'm testing, the API is called multiple times concurrently.
I'm using the following to log the Raw SQL being sent to the SQL Server:
context.Database.Log = Console.WriteLine;
When SQL is logged, it's getting mixed up with queries on other threads. More specifically, it's most-often the parameters which get mixed up. This makes it next to impossible to correlate the right parameters with the right query. Sometimes the same API is called twice concurrently.
I am using async calls, but that wouldn't be causing the issue. It will be the fact there are multiple concurrent web requests on different completion threads.
I need accurate reliable logging so I can look back in the output window and review the SQL.
You need to buffer all log messages per-context, then write out that buffer upon disposal of your db context.
You need to be able to hook into your db context's dispose event
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (OnDisposed != null) OnDisposed(this, null);
}
public event EventHandler OnDisposed;
Then you need this class to manage the buffering per-context
class LogGroup
{
static bool ReferenceActiveGroups = true; //I'm not sure if this is needed. It might work fine without.
static HashSet<LogGroup> LogGroups = ReferenceActiveGroups ? new HashSet<LogGroup>() : null;
/// <summary>
/// For the currently being ran query, this outputs the Raw SQL and the length of time it was executed in the Output window (CTRL + ALT + O) when in Debug mode.
/// </summary>
/// <param name="db">The DbContext to be outputted in the Output Window.</param>
public static void Log(ApiController context, AppContext db)
{
var o = new LogGroup(context, db);
o.Initialise();
if (ReferenceActiveGroups) o.Add();
}
public LogGroup(ApiController context, AppContext db)
{
this.context = context;
this.db = db;
}
public void Initialise()
{
db.OnDisposed += (sender, e) => { this.Complete(); };
db.Database.Log = this.Handler;
sb.AppendLine("LOG GROUP START");
}
public void Add()
{
lock (LogGroups)
{
LogGroups.Add(this);
}
}
public void Handler(string message)
{
sb.AppendLine(message);
}
public AppContext db = null;
public ApiController context = null;
public StringBuilder sb = new StringBuilder();
public void Remove()
{
lock (LogGroups)
{
LogGroups.Remove(this);
}
}
public void Complete()
{
if (ReferenceActiveGroups) Remove();
sb.AppendLine("LOG GROUP END");
System.Diagnostics.Debug.WriteLine(sb.ToString());
}
}
It should work without saving a strong reference to the LogGroup object. But I haven't tested that yet. Also, you could include this kind of code directly on the context, so you definitely won't need to save a LogGroup reference object. But that wouldn't be as portable.
To use it in a controller action funtion:
var db = new MyDbContext();
LogGroup.Log(this, db);
Note, that I pass the controller reference, so the log can include some extra context information - the request URI.
Interpreting your log
Now that the log works, you'll find the commented parameters in the log output are a pain to work with. You would normally have to manually change them to proper SQL parameters, but even then it's difficult to run sub-sections of a larger SQL query with parameters.
I know there are one or two other ways to get EF to output the log. Those methods do provide better control over how parameters are output, but given the answer is about making Database.Log work I'll include this tool in WinForms, so it can rewrite your clipboard with a functional query.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
class parameter
{
public string Name;
public string Value;
public string Type;
public string FormattedValue
{
get
{
if (Type == "Boolean")
{
if (Value == "True")
return "1";
else
return "0";
}
else if (Type == "Int32")
{
return Value;
}
else
throw new Exception("Unsupported type - " + Type);
}
}
public override string ToString()
{
return string.Format("{0} - {1} - {2} - {3}", Name, Value, Type, FormattedValue);
}
}
private void button1_Click(object sender, EventArgs e)
{
var sb = new StringBuilder();
var data = Clipboard.GetText(TextDataFormat.UnicodeText);
var lines = data.Split(new string[] { "\r\n" }, StringSplitOptions.None);
var parameters = GetParmeters(lines);
parameters.Reverse();
foreach (var item in lines)
{
if (item.Trim().Length == 0)
continue;
if (item.TrimStart().StartsWith("--"))
continue;
var SQLLine = item;
foreach (var p in parameters)
{
SQLLine = SQLLine.Replace("#" + p.Name, p.FormattedValue);
}
sb.AppendLine(SQLLine);
}
Clipboard.SetText(sb.ToString());
}
private static List<parameter> GetParmeters(string[] lines)
{
var parameters = new List<parameter>();
foreach (var item in lines)
{
var trimed = item.Trim();
if (trimed.StartsWith("-- p__linq__") == false)
continue;
var colonInd = trimed.IndexOf(':');
if (colonInd == -1)
continue;
var paramName = trimed.Substring(3, colonInd - 3);
var valueStart = colonInd + 3;
var valueEnd = trimed.IndexOf('\'', valueStart);
if (valueEnd == -1)
continue;
var value = trimed.Substring(valueStart, valueEnd - valueStart);
var typeStart = trimed.IndexOf("(Type = ");
if (typeStart == -1)
continue;
typeStart += 8;
var typeEnd = trimed.IndexOf(',', typeStart);
if (typeEnd == -1)
typeEnd = trimed.IndexOf(')', typeStart);
if (typeEnd == -1)
continue;
var type = trimed.Substring(typeStart, typeEnd - typeStart);
var param = new parameter();
param.Name = paramName;
param.Value = value;
param.Type = type;
parameters.Add(param);
}
return parameters;
}
}
I'm getting an odd issue where I'm able to return results from a call to a stored procedure, but the code retrospectively fails.
public IEnumerable<T> ExecuteStoredProcedure<T>(string storedProcedureName, IDataMapper<T> mapper, IDictionary<string, object> parameters)
{
using (var connection = new SqlConnection(connectionString))
{
using (var cmd = new SqlCommand(storedProcedureName, connection))
{
cmd.CommandType = CommandType.StoredProcedure;
foreach (var key in parameters.Keys)
{
cmd.Parameters.AddWithValue(key, parameters[key]);
}
connection.Open();
SqlDataReader reader = cmd.ExecuteReader();
//return MapRecordsToDTOs(reader, mapper);
//let's test:
IEnumerable<T> result = MapRecordsToDTOs(reader, mapper);
var x = (new List<T>(result)).Count;
System.Diagnostics.Debug.WriteLine(x);
return result;
}
}
}
private static IEnumerable<T> MapRecordsToDTOs<T>(SqlDataReader reader, IDataMapper<T> mapper)
{
if (reader.HasRows)
{
while (reader.Read())
{
System.Diagnostics.Debug.WriteLine(reader["Id"]); //what's going on...
yield return mapper.MapToDto((IDataRecord)reader);
}
}
}
Calling this code shows that variable x always represents the number of rows I'd expect to see from a call to my stored procedures.
Additionally my debug output shows the ID values I'd expect to see.
However, after those results are returned, I get the error An exception of type 'System.InvalidOperationException' occurred in System.Data.dll but was not handled in user code from the line if (reader.HasRows) (i.e. which has already been executed). The browser from which I invoke this request shows HTTP Error 502.3 - Bad Gateway.
I suspect the reason is the system's calculating the ID and X values for debug separately to how it would return the real user output. As such, it performs a lazy operation to get the IEnumerable values at the point at which it has to return them; only by this point the using statements have caused the dispose methods to be called, and thus the reader's connection is null (this is what I see when I inspect the reader variable's properties whilst debugging).
Has anyone seen behaviour like this before / is it a bug; or have I just missed something obvious?
Additional Code:
public interface IDataMapper<T>
{
T MapToDto(IDataRecord record);
}
public class CurrencyMapper: IDataMapper<CurrencyDTO>
{
const string FieldNameCode = "Code";
const string FieldNameId = "Id";
const string FieldNameName = "Name";
const string FieldNameNum = "Num";
const string FieldNameE = "E";
const string FieldNameSymbol = "Symbol";
public CurrencyMapper() { }
public CurrencyDTO MapToDto(IDataRecord record)
{
var code = record[FieldNameCode] as string;
var id = record[FieldNameId] as Guid?;
var name = record[FieldNameName] as string;
var num = record[FieldNameNum] as string;
var e = record[FieldNameE] as int?;
var symbol = record[FieldNameSymbol] as char?;
return new CurrencyDTO(id, code, num, e, name, symbol);
}
}
public class CurrencyRepository
{
const string SPReadAll = "usp_CRUD_Currency_ReadAll";
readonly SqlDatabase db;
public CurrencyRepository()
{
db = new SqlDatabase(); //stick to SQL only for the moment for simplicity
}
public IEnumerable<CurrencyDTO> GetCurrencyCodes()
{
var mapper = new CurrencyMapper();
return db.ExecuteStoredProcedure(SPReadAll, mapper);
}
}
public class CurrencyDTO
{
readonly Guid? id;
readonly string code;
readonly string num;
readonly int? e;
readonly string name;
readonly char? symbol;
public CurrencyDTO(Guid? id,string code,string num,int? e,string name, char? symbol)
{
this.id = id;
this.code = code;
this.num = num;
this.e = e;
this.name = name;
this.symbol = symbol;
}
public Guid? Id { get { return id; } }
public string Code { get { return code; } }
public string Num { get { return num; } }
public int? E { get { return e; } }
public string Name { get { return name; } }
public char? Symbol { get { return symbol; } }
}
I've temporarily implemented a workaround which resolves this issue.
This works:
private static IEnumerable<T> MapRecordsToDTOs<T>(SqlDataReader reader, IDataMapper<T> mapper)
{
var list = new List<T>(); //use a list to force eager evaluation
if (reader.HasRows)
{
while (reader.Read())
{
list.Add(mapper.MapToDto((IDataRecord)reader));
}
}
return list.ToArray();
}
As opposed to the original:
private static IEnumerable<T> MapRecordsToDTOs<T>(SqlDataReader reader, IDataMapper<T> mapper)
{
if (reader.HasRows)
{
while (reader.Read())
{
yield return mapper.MapToDto((IDataRecord)reader);
}
}
}
The difference being I move the code affected by the iterator such that it only iterates through the results in the list; and doesn't rely on the compiler sensibly understanding the requirements related to IDisposable objects.
It's my understanding that the compiler should be able to handle this for me (confirmed here: https://stackoverflow.com/a/13504789/361842), so I suspect it's a bug in the compiler.
Reported here: https://connect.microsoft.com/VisualStudio/feedback/details/3113138
Additional demo code here:
https://gist.github.com/JohnLBevan/a910d886df577e442e2f5a9c2dd41293/
I am new to MongoDB and setting up a .NET C# MVC Project to try it out.
Using the current MongoDB.Driver 2.2.2 from nuget.
Issue:
I have a unit test that succeeds but does not save any data to the MongoDB Collection.
[TestMethod]
public void TestMethod1()
{
IDatabase<Publisher> context = new MyProject.Data.Concrete.MongoDatabase<Publisher>("Publishers");
Publisher pub = new Publisher()
{
Name = "Test"
};
context.Add(pub);
}
However, if I put this same test under a MVC Controller it will save the data:
public ActionResult Contact() {
ViewBag.Message = "Your contact page.";
IDatabase<Publisher> context = new MyProject.Data.Concrete.MongoDatabase<Publisher>("Publishers");
Publisher pub = new Publisher()
{
Name = "Test"
};
context.Add(pub);
return View();
}
I am curious as to why this is so?
Here is the MongoDatabase class:
public class MongoDatabase<T> : IDatabase<T> where T : class, new()
{
private static string _connectionString = ConfigurationManager.ConnectionStrings["mongodb"].ConnectionString;
private MongoClient _mongoClient = new MongoClient(_connectionString);
private string _collectionName;
private IMongoDatabase _db;
protected IMongoCollection<T> _collection
{
get
{
return _db.GetCollection<T>(_collectionName);
}
set
{
_collection = value;
}
}
public IQueryable<T> Query
{
get
{
return _collection.AsQueryable<T>();
}
set
{
Query = value;
}
}
public MongoDatabase(string collectionName)
{
_collectionName = collectionName;
_db = _mongoClient.GetDatabase(MongoUrl.Create(_connectionString).DatabaseName);
}
public bool Add(T item)
{
var result = _collection.InsertOneAsync(item);
//return result. what can we use here?
return true;
}
public int Add(IEnumerable<T> items)
{
int count = 0;
foreach (T item in items)
{
if (Add(item))
{
count++;
}
}
return count;
}
}
1) You should change calls for async methods.
public async Task Add(T item)
{
await _collection.InsertOneAsync(item);
}
You can read more about async.
2)
//return result. what can we use here?
return true;
You don't need to return bool. You need to have exception handler on a top level.
I have an application which caches some data at startup. There are several things to put in a cache, but they are very similar. I created classes like this, the only difference in them is the type of the item to be added to the dictionary (in this example the Setting class), and the _sqlNotifyCommand.CommandText.
public class SettingsCache : ILoggerClass
{
private Dictionary<int, Dictionary<int, Setting>> _cachedItems;
private string _entityConnectionString;
private SQLNotifier _sqlNotifier;
private SqlCommand _sqlNotifyCommand = new SqlCommand();
private bool _dataLoaded = false;
private void AddItem(Setting item)
{
if (!_cachedItems.ContainsKey(item.PartnerId))
{
_cachedItems.Add(item.PartnerId, new Dictionary<int, Setting>());
}
if (_cachedItems[item.PartnerId].ContainsKey(item.Id))
{
_cachedItems[item.PartnerId].Remove(item.Id);
}
_cachedItems[item.PartnerId].Add(item.Id, item);
}
public Setting GetSetting(int partnerId, int id)
{
if (_cachedItems.ContainsKey(partnerId))
{
if (_cachedItems[partnerId].ContainsKey(id))
{
return _cachedItems[partnerId][id];
}
return null;
}
return null;
}
public SettingsCache(string connectionString)
{
_entityConnectionString = connectionString;
_cachedItems = new Dictionary<int, Dictionary<int, Setting>>();
LoadData();
try
{
using (var db = new partnerEntity(connectionString))
{
string adoSqlConnectionString = ((EntityConnection) db.Connection).StoreConnection.ConnectionString;
_sqlNotifier = new SQLNotifier(adoSqlConnectionString);
_sqlNotifier.NewMessage += _sqlNotifier_NewMessage;
_sqlNotifyCommand.CommandType = CommandType.Text;
_sqlNotifyCommand.CommandText = "SELECT setting_id, setting_value, partner_id FROM dbo.setting";
_sqlNotifyCommand.Notification = null;
_sqlNotifier.RegisterDependency(_sqlNotifyCommand);
}
}
catch (Exception exception)
{
this.Log(this, LogLevel.Error, 0, exception);
}
}
private void _sqlNotifier_NewMessage(object sender, SqlNotificationEventArgs e)
{
if (e.Info == SqlNotificationInfo.Insert || e.Info == SqlNotificationInfo.Update)
{
this.Log(this, LogLevel.Info, 0, string.Format("Database changed, reloading settings data..."));
LoadData();
}
_sqlNotifier.RegisterDependency(_sqlNotifyCommand);
}
private void LoadData()
{
_dataLoaded = false;
try
{
using (var db = new partnerEntity(_entityConnectionString))
{
var dbData = db.setting.ToList();
foreach (var cItem in dbData)
{
AddItem(new Setting
{
PartnerId = cItem.partner_id,
Id = cItem.setting_id,
Value = cItem.setting_value
});
}
}
_dataLoaded = true;
}
catch (Exception exception)
{
this.Log(this, LogLevel.Error, 0, exception);
}
if (!_dataLoaded)
{
Task.Delay(TimeSpan.FromSeconds(10)).ContinueWith(_ => { LoadData(); });
}
}
}
Is there a more generic way to do this? The last thing which was needed in the classes this part:
if (!_dataLoaded)
{
Task.Delay(TimeSpan.FromSeconds(10)).ContinueWith(_ => { LoadData(); });
}
And I had to modify every Caching class. I needed to declare the variable, add it to the try-catch block, and after the block insert the same line in 6 classes. This code seems very boilerplate to me, I can't believe there is no simpler solution. I tried to make an Interface with AddItem, LoadData, OnLoadDataFailed methods, but in the AddItem method I need to specify the item, I'm stuck.
Here is the basic structure of what (I think) you want to accomplish using Generics.
First, declare a common interface for cached items so that the AddItem method still works:
public interface ICacheItem
{
int Id { get; set; }
int PartnerId { get; set; }
}
Now you can create the SettingsCache<T> that holds the items:
public class SettingsCache<T> : ILogger where T : ICacheItem
{
private Dictionary<int, Dictionary<int, T>> _cachedItems;
//...
public SettingsCache(string connectionString, string commandText)
{
//...
}
private void AddItem(T item)
{
if (!_cachedItems.ContainsKey(item.PartnerId))
{
_cachedItems.Add(item.PartnerId, new Dictionary<int, T>());
}
if (_cachedItems[item.PartnerId].ContainsKey(item.Id))
{
_cachedItems[item.PartnerId].Remove(item.Id);
}
_cachedItems[item.PartnerId].Add(item.Id, item);
}
}
I left out most of the implementation to focus on your two biggest concerns, the command text and the AddItem method. The generic has a constraint so that it can only accept items that are of ICacheItem, so you can use any properties that ICacheItem defines.
To use a cache, simply create one with the specific type (assuming Setting implements ICacheItem):
var settingsCache = new SettingsCache<Setting>("connection string", "command string");