Messages are being mixed up through Database.Log by multiple threads - c#

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;
}
}

Related

Code does not step in specific method

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;
}

Better approach for saving data for multiple tables using entity framework

I am currently using the following approach for saving my data in multiple tables in database, which I am extracting from excel files.
public class Saver
{
public static int SaveCensusBatch(string key, ICollection<tbl_Life_Census> collection)
{
using (var db = new AuraEntities())
{
var entry = new tbl_Life_Master() { UUID = key, tbl_Life_Census = collection };
db.tbl_Life_Master.Add(entry);
db.SaveChanges();
return 1;
}
}
public static int SaveLifeData(string key2, ICollection<tbl_Life_General_Info> collection)
{
using (var db = new AuraEntities())
{
var entry = new tbl_Life_Master() { UUID = key2, tbl_Life_General_Info = collection };
db.tbl_Life_Master.Add(entry);
db.SaveChanges();
return 1;
}
}
public static T GetDBRecordByPK<T>(string key) where T : class
{
using (var db = new AuraEntities())
{
var t = db.Set<T>().Find(key);
return t;
}
}
}
Following is the code for calling this in main:
foreach (var r in results)
{
r.UUID = key.ToString();
}
Saver.SaveCensusBatch(key.ToString(), results);
Saver.SaveLifeData(key.ToString(), results3);
var master = Saver.GetDBRecordByPK<tbl_Life_Master>(key.ToString());
Please suggest me how can I do everything under one 'using block' and one function only instead of implementing several functions. This is because I have to insert data into 20-30 tables simultaneously.
You can do it by making all the Saver class' methods non-static (it also prevents keeping data in memory when you don't need it), and make it implement IDisposable interface. After that, you only need to follow the disposable pattern that Microsoft suggests (https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx), for example:
public class Saver : IDisposable
{
private readonly AuraEntities db;
private bool disposed;
public Saver()
{
db = new AuraEntities();
disposed = false;
}
public int SaveCensusBatch(string key, ICollection<tbl_Life_Census> collection)
{
var entry = new tbl_Life_Master() { UUID = key, tbl_Life_Census = collection };
db.tbl_Life_Master.Add(entry);
return 1;
}
public int SaveLifeData(string key2, ICollection<tbl_Life_General_Info> collection)
{
var entry = new tbl_Life_Master() { UUID = key2, tbl_Life_General_Info = collection };
db.tbl_Life_Master.Add(entry);
return 1;
}
public T GetDBRecordByPK<T>(string key) where T : class
{
var t = db.Set<T>().Find(key);
return t;
}
public void Save()
{
db.SaveChanges();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing && db != null)
{
db.Dispose();
disposed = true;
}
}
}
And then calling the methods within an using statement like this:
using(var saver = new Saver())
{
saver.SaveCensusBatch(key.ToString(), results);
saver.SaveLifeData(key.ToString(), results3);
saver.Save();
}
Updated: It is better if you save from outside the saver class. Because he whole process will behave as a single transaction, and it won't be persisted if an exception is raised.
You could select your results into a List and pass the list to the function. In that save function you could do your loop for each UUID and have the code for the three previous functions within the loop. Then call the db.SaveChanges() at the end outside the loop. The db.saveChanges() works as a transaction and would rollback if there was an error at any point during the save.

Caching WebAPI 2

EDIT: For each request, a new instance of controller is created. However, this is not true with Attribute classes. Once they are created, it is used for multiple requests. I hope it helps.
I wrote my own WebAPI (using latest version of WebAPI and .net framework) caching action filter. I am aware about CacheCow & this. However, i wanted mine anyways.
However, there is some issue with my code because i don't get exepected output when i use it in my project on live server. On local machine everything works fine.
I used below code in my blog RSS generator and i cache the data for each category. There are around 5 categories (food, tech, personal etc).
Issue: When i navigate to say api/GetTech it returns me the rss feed items from personal blog category. When i navigate to say api/GetPersonal , it returns me api/Food
I am not able to find the root cause but I think this is due to use of static method/variable. I have double checked that my _cachekey has unique value for each category of my blog.
Can someone point out any issues with this code esp when we have say 300 requests per minute ?
public class WebApiOutputCacheAttribute : ActionFilterAttribute
{
// Cache timespan
private readonly int _timespan;
// cache key
private string _cachekey;
// cache repository
private static readonly MemoryCache _webApiCache = MemoryCache.Default;
/// <summary>
/// Initializes a new instance of the <see cref="WebApiOutputCacheAttribute"/> class.
/// </summary>
/// <param name="timespan">The timespan in seconds.</param>
public WebApiOutputCacheAttribute(int timespan)
{
_timespan = timespan;
}
public override void OnActionExecuting(HttpActionContext ac)
{
if (ac != null)
{
_cachekey = ac.Request.RequestUri.PathAndQuery.ToUpperInvariant();
if (!_webApiCache.Contains(_cachekey)) return;
var val = (string)_webApiCache.Get(_cachekey);
if (val == null) return;
ac.Response = ac.Request.CreateResponse();
ac.Response.Content = new StringContent(val);
var contenttype = (MediaTypeHeaderValue)_webApiCache.Get("response-ct") ?? new MediaTypeHeaderValue("application/rss+xml");
ac.Response.Content.Headers.ContentType = contenttype;
}
else
{
throw new ArgumentNullException("ac");
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
if (_webApiCache.Contains(_cachekey)) return;
var body = actionExecutedContext.Response.Content.ReadAsStringAsync().Result;
if (actionExecutedContext.Response.StatusCode == HttpStatusCode.OK)
{
lock (WebApiCache)
{
_wbApiCache.Add(_cachekey, body, DateTime.Now.AddSeconds(_timespan));
_webApiCache.Add("response-ct", actionExecutedContext.Response.Content.Headers.ContentType, DateTimeOffset.UtcNow.AddSeconds(_timespan));
}
}
}
}
The same WebApiOutputCacheAttribute instance can be used to cache multiple simultaneous requests, so you should not store cache keys on the instance of the attribute. Instead, regenerate the cache key during each request / method override. The following attribute works to cache HTTP GET requests.
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Newtonsoft.Json;
// based on strathweb implementation
// http://www.strathweb.com/2012/05/output-caching-in-asp-net-web-api/
public class CacheHttpGetAttribute : ActionFilterAttribute
{
public int Duration { get; set; }
public ILogExceptions ExceptionLogger { get; set; }
public IProvideCache CacheProvider { get; set; }
private bool IsCacheable(HttpRequestMessage request)
{
if (Duration < 1)
throw new InvalidOperationException("Duration must be greater than zero.");
// only cache for GET requests
return request.Method == HttpMethod.Get;
}
private CacheControlHeaderValue SetClientCache()
{
var cachecontrol = new CacheControlHeaderValue
{
MaxAge = TimeSpan.FromSeconds(Duration),
MustRevalidate = true,
};
return cachecontrol;
}
private static string GetServerCacheKey(HttpRequestMessage request)
{
var acceptHeaders = request.Headers.Accept;
var acceptHeader = acceptHeaders.Any() ? acceptHeaders.First().ToString() : "*/*";
return string.Join(":", new[]
{
request.RequestUri.AbsoluteUri,
acceptHeader,
});
}
private static string GetClientCacheKey(string serverCacheKey)
{
return string.Join(":", new[]
{
serverCacheKey,
"response-content-type",
});
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext == null) throw new ArgumentNullException("actionContext");
var request = actionContext.Request;
if (!IsCacheable(request)) return;
try
{
// do NOT store cache keys on this attribute because the same instance
// can be reused for multiple requests
var serverCacheKey = GetServerCacheKey(request);
var clientCacheKey = GetClientCacheKey(serverCacheKey);
if (CacheProvider.Contains(serverCacheKey))
{
var serverValue = CacheProvider.Get(serverCacheKey);
var clientValue = CacheProvider.Get(clientCacheKey);
if (serverValue == null) return;
var contentType = clientValue != null
? JsonConvert.DeserializeObject<MediaTypeHeaderValue>(clientValue.ToString())
: new MediaTypeHeaderValue(serverCacheKey.Substring(serverCacheKey.LastIndexOf(':') + 1));
actionContext.Response = actionContext.Request.CreateResponse();
// do not try to create a string content if the value is binary
actionContext.Response.Content = serverValue is byte[]
? new ByteArrayContent((byte[])serverValue)
: new StringContent(serverValue.ToString());
actionContext.Response.Content.Headers.ContentType = contentType;
actionContext.Response.Headers.CacheControl = SetClientCache();
}
}
catch (Exception ex)
{
ExceptionLogger.Log(ex);
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
try
{
var request = actionExecutedContext.Request;
// do NOT store cache keys on this attribute because the same instance
// can be reused for multiple requests
var serverCacheKey = GetServerCacheKey(request);
var clientCacheKey = GetClientCacheKey(serverCacheKey);
if (!CacheProvider.Contains(serverCacheKey))
{
var contentType = actionExecutedContext.Response.Content.Headers.ContentType;
object serverValue;
if (contentType.MediaType.StartsWith("image/"))
serverValue = actionExecutedContext.Response.Content.ReadAsByteArrayAsync().Result;
else
serverValue = actionExecutedContext.Response.Content.ReadAsStringAsync().Result;
var clientValue = JsonConvert.SerializeObject(
new
{
contentType.MediaType,
contentType.CharSet,
});
CacheProvider.Add(serverCacheKey, serverValue, new TimeSpan(0, 0, Duration));
CacheProvider.Add(clientCacheKey, clientValue, new TimeSpan(0, 0, Duration));
}
if (IsCacheable(actionExecutedContext.Request))
actionExecutedContext.ActionContext.Response.Headers.CacheControl = SetClientCache();
}
catch (Exception ex)
{
ExceptionLogger.Log(ex);
}
}
}
Just replace the CacheProvider with your MemoryCache.Default. In fact, the code above uses the same by default during development, and uses azure cache when deployed to a live server.
Even though your code resets the _cachekey instance field during each request, these attributes are not like controllers where a new one is created for each request. Instead, the attribute instance can be repurposed to service multiple simultaneous requests. So don't use an instance field to store it, regenerate it based on the request each and every time you need it.

C# handle instance of an object after continue in foreach loop

Let's assume I have an instance of this class.
public class MyClass
{
public string LocationCode;
public string PickUpCode
}
There is another class that takes a List<MyClass> as input and saves to the DB.
Now I need to apply some business rules:
For example if LocationCode is null this item in the List<MyClass> must be skipped and the foreach loop must continue to the next item in the list.
I've written the following code and the items with null LocationCode are indeed skipped but the var instance = new SomeClass(); somehow remains in memory so when the loop reaches an valid item and proceeds to save it in the DB, it also saves all the previously skipped instances of var instance = new SomeClass();. Which means I have null entries in the DB.
I'm using NHibernate and Evictdoesn't seam to be doing the trick. Any suggestions?
public void Save(List<MyClass> listOfItems)
{
using (UnitOfWork.Start())
{
var repository = new Repository();
try
{
foreach (var item in listOfItems.Select(i => i.Item).Where(item => item != null))
{
var instance = new SomeClass();
if (pickUpCode != null)
{
instance.PickUpCode = pickUpCode;
}
else
{
instance.PickUpCode = null;
}
if (locationCode != null)
{
instance.StartLocation = locationCode
}
else
{
UnitOfWork.CurrentSession.Evict(instance);
continue;
}
repository.SaveSomeClass(instance);
}
}
catch (Exception ex)
{
_log.Error(" Unhandled error", ex);
}
}
}
** Because someone asked, here's some code on UnitOfWork.Start()
public static class UnitOfWork
{
public static IUnitOfWork Start();
}
public interface IUnitOfWork : IDisposable
{
bool IsInActiveTransaction { get; }
IUnitOfWorkFactory SessionFactory { get; }
IGenericTransaction BeginTransaction();
IGenericTransaction BeginTransaction(IsolationLevel isolationLevel);
void Flush();
void TransactionalFlush();
void TransactionalFlush(IsolationLevel isolationLevel);
}
Why don't you fail first and avoid all of this?
Example being:
foreach (var item in listOfItems.Select(i => i.Item).Where(item => item != null))
{
if (item.LocationCode == null){
continue;
}
var instance = new SomeClass();
if (pickUpCode != null)
{
instance.PickUpCode = pickUpCode;
}
else
{
instance.PickUpCode = null;
}
// if we reach here, location code is definitley not null, no need for the check
instance.StartLocation = locationCode
repository.SaveSomeClass(instance);
}
Alternatively, you could add the check to you LINQ where clause
foreach (var item in listOfItems.where(item=> item != null && item.LocationCode != null)
Without more code on how UnitofWork.Start works its hard to suggest. But, It's worth trying by implementing IDisposable on SomeClass.

Your Thoughts: Entity Framework Data Context via a DataHelper class (centralized context and transactions) [closed]

This question is unlikely to help any future visitors; it is only relevant to a small geographic area, a specific moment in time, or an extraordinarily narrow situation that is not generally applicable to the worldwide audience of the internet. For help making this question more broadly applicable, visit the help center.
Closed 10 years ago.
I want to get opinions on the code I have put together for a centralized DataContext via a DataHelper class that I have created to be re-used on projects.
NOTE - there is a ton of code here, sorry about that, but I really wanted to layout out the complete approach and uses for my ideas. I'm an not saying this is the right approach, but it works for me so far (still playing with the approach, nothing in production yet, but very similar to stuff I have built over the years) and I really want to get constructive feedback from the community on what I have built to see if it is insane, great, can be improved, etc...
A few thoughts I put into this:
Data Context needs to be stored in a common memory space, easily accessible
Transactions should take the same approach
It must be disposed of properly
Allows for better separation of business logic for Saving and Deleting in transactions.
Here is the code for each item:
1 - First the data context stored in either the current HttpContext.Current.Items collection (so it only lives for the life of the page and only is fired up once at the first requested) or if the HttpContext doesn't exist uses a ThreadSlot (in which case that code most clean it up itself, like a console app using it...):
public static class DataHelper {
/// <summary>
/// Current Data Context object in the HTTPContext or Current Thread
/// </summary>
public static TemplateProjectContext Context {
get {
TemplateProjectContext context = null;
if (HttpContext.Current == null) {
LocalDataStoreSlot threadSlot = Thread.GetNamedDataSlot("DataHelper.CurrentContext");
if (Thread.GetData(threadSlot) == null) {
context = new TemplateProjectContext();
Thread.SetData(threadSlot, context);
} else {
context = (TemplateProjectContext)Thread.GetData(threadSlot);
}
} else {
if (HttpContext.Current.Items["DataHelper.CurrentContext"] == null) {
context = new TemplateProjectContext();
HttpContext.Current.Items["DataHelper.CurrentContext"] = context;
} else {
context = (TemplateProjectContext)HttpContext.Current.Items["DataHelper.CurrentContext"];
}
}
return context;
}
set {
if (HttpContext.Current == null) {
if (value == null) {
Thread.FreeNamedDataSlot("DataHelper.CurrentContext");
} else {
LocalDataStoreSlot threadSlot = Thread.GetNamedDataSlot("DataHelper.CurrentContext");
Thread.SetData(threadSlot, value);
}
} else {
if (value == null)
HttpContext.Current.Items.Remove("DataHelper.CurrentContext");
else
HttpContext.Current.Items["DataHelper.CurrentContext"] = value;
}
}
}
...
2 - To support transactions, I use a similar approach, and also include helper methods to Begin, Commit and Rollback:
/// <summary>
/// Current Transaction object in the HTTPContext or Current Thread
/// </summary>
public static DbTransaction Transaction {
get {
if (HttpContext.Current == null) {
LocalDataStoreSlot threadSlot = Thread.GetNamedDataSlot("currentTransaction");
if (Thread.GetData(threadSlot) == null) {
return null;
} else {
return (DbTransaction)Thread.GetData(threadSlot);
}
} else {
if (HttpContext.Current.Items["currentTransaction"] == null) {
return null;
} else {
return (DbTransaction)HttpContext.Current.Items["currentTransaction"];
}
}
}
set {
if (HttpContext.Current == null) {
LocalDataStoreSlot threadSlot = Thread.GetNamedDataSlot("currentTransaction");
Thread.SetData(threadSlot, value);
} else {
HttpContext.Current.Items["currentTransaction"] = value;
}
}
}
/// <summary>
/// Begins a transaction based on the common connection and transaction
/// </summary>
public static void BeginTransaction() {
DataHelper.Transaction = DataHelper.CreateSqlTransaction();
}
/// <summary>
/// Creates a SqlTransaction object based on the current common connection
/// </summary>
/// <returns>A new SqlTransaction object for the current common connection</returns>
public static DbTransaction CreateSqlTransaction() {
return CreateSqlTransaction(DataHelper.Context.Connection);
}
/// <summary>
/// Creates a SqlTransaction object for the requested connection object
/// </summary>
/// <param name="connection">Reference to the connection object the transaction should be created for</param>
/// <returns>New transaction object for the requested connection</returns>
public static DbTransaction CreateSqlTransaction(DbConnection connection) {
if (connection.State != ConnectionState.Open) connection.Open();
return connection.BeginTransaction();
}
/// <summary>
/// Rolls back and cleans up the current common transaction
/// </summary>
public static void RollbackTransaction() {
if (DataHelper.Transaction != null) {
DataHelper.RollbackTransaction(DataHelper.Transaction);
if (HttpContext.Current == null) {
Thread.FreeNamedDataSlot("currentTransaction");
} else {
HttpContext.Current.Items.Remove("currentTransaction");
}
}
}
/// <summary>
/// Rolls back and disposes of the requested transaction
/// </summary>
/// <param name="transaction">The transaction to rollback</param>
public static void RollbackTransaction(DbTransaction transaction) {
transaction.Rollback();
transaction.Dispose();
}
/// <summary>
/// Commits and cleans up the current common transaction
/// </summary>
public static void CommitTransaction() {
if (DataHelper.Transaction != null) {
DataHelper.CommitTransaction(DataHelper.Transaction);
if (HttpContext.Current == null) {
Thread.FreeNamedDataSlot("currentTransaction");
} else {
HttpContext.Current.Items.Remove("currentTransaction");
}
}
}
/// <summary>
/// Commits and disposes of the requested transaction
/// </summary>
/// <param name="transaction">The transaction to commit</param>
public static void CommitTransaction(DbTransaction transaction) {
transaction.Commit();
transaction.Dispose();
}
3 - Clean and easy Disposal
/// <summary>
/// Cleans up the currently active connection
/// </summary>
public static void Dispose() {
if (HttpContext.Current == null) {
LocalDataStoreSlot threadSlot = Thread.GetNamedDataSlot("DataHelper.CurrentContext");
if (Thread.GetData(threadSlot) != null) {
DbTransaction transaction = DataHelper.Transaction;
if (transaction != null) {
DataHelper.CommitTransaction(transaction);
Thread.FreeNamedDataSlot("currentTransaction");
}
((TemplateProjectContext)Thread.GetData(threadSlot)).Dispose();
Thread.FreeNamedDataSlot("DataHelper.CurrentContext");
}
} else {
if (HttpContext.Current.Items["DataHelper.CurrentContext"] != null) {
DbTransaction transaction = DataHelper.Transaction;
if (transaction != null) {
DataHelper.CommitTransaction(transaction);
HttpContext.Current.Items.Remove("currentTransaction");
}
((TemplateProjectContext)HttpContext.Current.Items["DataHelper.CurrentContext"]).Dispose();
HttpContext.Current.Items.Remove("DataHelper.CurrentContext");
}
}
}
3b - I'm building this in MVC, so I have a "base" Controller class that all my controllers inherit from - this way the Context only lives from when first accessed on a request, and until the page is disposed, that way its not too "long running"
using System.Web.Mvc;
using Core.ClassLibrary;
using TemplateProject.Business;
using TemplateProject.ClassLibrary;
namespace TemplateProject.Web.Mvc {
public class SiteController : Controller {
protected override void Dispose(bool disposing) {
DataHelper.Dispose();
base.Dispose(disposing);
}
}
}
4 - So I am big on business classes, separation of concerns, reusable code, all that wonderful stuff. I have an approach that I call "Entity Generic" that can be applied to any entity in my system - for example, Addresses and Phones
A Customer can have 1 or more of each, along with a Store, Person, or anything really - so why add street, city, state, etc to every thing that needs it when you can just build an Address entity, that takes a Foreign Type and Key (what I call EntityType and EntityId) - then you have a re-usable business object, supporting UI control, etc - so you build it once and re-use it everywhere.
This is where the centralized approach I am pushing for here really comes in handy and I think makes the code much cleaner than having to pass the current data context/transaction into every method.
Take for example that you have a Page for a customer, the Model includes the Customer data, Contact, Address and a few Phone Numbers (main, fax, or cell, whatever)
When getting a Customer Edit Model for the page, here is a bit of the code I have put together (see how I use the DataHelper.Context in the LINQ):
public static CustomerEditModel FetchEditModel(int customerId) {
if (customerId == 0) {
CustomerEditModel model = new CustomerEditModel();
model.MainContact = new CustomerContactEditModel();
model.MainAddress = new AddressEditModel();
model.ShippingAddress = new AddressEditModel();
model.Phone = new PhoneEditModel();
model.Cell = new PhoneEditModel();
model.Fax = new PhoneEditModel();
return model;
} else {
var output = (from c in DataHelper.Context.Customers
where c.CustomerId == customerId
select new CustomerEditModel {
CustomerId = c.CustomerId,
CompanyName = c.CompanyName
}).SingleOrDefault();
if (output != null) {
output.MainContact = CustomerContact.FetchEditModelByPrimary(customerId) ?? new CustomerContactEditModel();
output.MainAddress = Address.FetchEditModelByType(BusinessEntityTypes.Customer, customerId, AddressTypes.Main) ?? new AddressEditModel();
output.ShippingAddress = Address.FetchEditModelByType(BusinessEntityTypes.Customer, customerId, AddressTypes.Shipping) ?? new AddressEditModel();
output.Phone = Phone.FetchEditModelByType(BusinessEntityTypes.Customer, customerId, PhoneTypes.Main) ?? new PhoneEditModel();
output.Cell = Phone.FetchEditModelByType(BusinessEntityTypes.Customer, customerId, PhoneTypes.Cell) ?? new PhoneEditModel();
output.Fax = Phone.FetchEditModelByType(BusinessEntityTypes.Customer, customerId, PhoneTypes.Fax) ?? new PhoneEditModel();
}
return output;
}
}
And here is a sample of the the phone returning the Edit model to be used:
public static PhoneEditModel FetchEditModelByType(byte entityType, int entityId, byte phoneType) {
return (from p in DataHelper.Context.Phones
where p.EntityType == entityType
&& p.EntityId == entityId
&& p.PhoneType == phoneType
select new PhoneEditModel {
PhoneId = p.PhoneId,
PhoneNumber = p.PhoneNumber,
Extension = p.Extension
}).FirstOrDefault();
}
Now the page has posted back and this all needs to be save, so the Save method in my control just lets the business object handle this all:
[Authorize(Roles = SiteRoles.SiteAdministrator + ", " + SiteRoles.Customers_Edit)]
[HttpPost]
public ActionResult Create(CustomerEditModel customer) {
return CreateOrEdit(customer);
}
[Authorize(Roles = SiteRoles.SiteAdministrator + ", " + SiteRoles.Customers_Edit)]
[HttpPost]
public ActionResult Edit(CustomerEditModel customer) {
return CreateOrEdit(customer);
}
private ActionResult CreateOrEdit(CustomerEditModel customer) {
if (ModelState.IsValid) {
SaveResult result = Customer.SaveEditModel(customer);
if (result.Success) {
return RedirectToAction("Index");
} else {
foreach (KeyValuePair<string, string> error in result.ErrorMessages) ModelState.AddModelError(error.Key, error.Value);
}
}
return View(customer);
}
And inside the Customer business object - it handles the transaction centrally and lets the Contact, Address and Phone business classes do their thing and really not worry about the transaction:
public static SaveResult SaveEditModel(CustomerEditModel model) {
SaveResult output = new SaveResult();
DataHelper.BeginTransaction();
try {
Customer customer = null;
if (model.CustomerId == 0) customer = new Customer();
else customer = DataHelper.Context.Customers.Single(c => c.CustomerId == model.CustomerId);
if (customer == null) {
output.Success = false;
output.ErrorMessages.Add("CustomerNotFound", "Unable to find the requested Customer record to update");
} else {
customer.CompanyName = model.CompanyName;
if (model.CustomerId == 0) {
customer.SiteGroup = CoreSession.CoreSettings.CurrentSiteGroup;
customer.CreatedDate = DateTime.Now;
customer.CreatedBy = SiteLogin.Session.ActiveUser;
DataHelper.Context.Customers.AddObject(customer);
} else {
customer.ModifiedDate = DateTime.Now;
customer.ModifiedBy = SiteLogin.Session.ActiveUser;
}
DataHelper.Context.SaveChanges();
SaveResult result = Address.SaveEditModel(model.MainAddress, BusinessEntityTypes.Customer, customer.CustomerId, AddressTypes.Main, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
result = Address.SaveEditModel(model.ShippingAddress, BusinessEntityTypes.Customer, customer.CustomerId, AddressTypes.Shipping, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
result = Phone.SaveEditModel(model.Phone, BusinessEntityTypes.Customer, customer.CustomerId, PhoneTypes.Main, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
result = Phone.SaveEditModel(model.Fax, BusinessEntityTypes.Customer, customer.CustomerId, PhoneTypes.Fax, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
result = Phone.SaveEditModel(model.Cell, BusinessEntityTypes.Customer, customer.CustomerId, PhoneTypes.Cell, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
result = CustomerContact.SaveEditModel(model.MainContact, customer.CustomerId, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
if (output.Success) {
DataHelper.Context.SaveChanges();
DataHelper.CommitTransaction();
} else {
DataHelper.RollbackTransaction();
}
}
} catch (Exception exp) {
DataHelper.RollbackTransaction();
ErrorHandler.Handle(exp, true);
output.Success = false;
output.ErrorMessages.Add(exp.GetType().ToString(), exp.Message);
output.Exceptions.Add(exp);
}
return output;
}
Notice how each Address, Phone, Etc is handled by its own business class, here is the Phone's save method - notice how it doesn't actually do the save here unless you tell it to (save is handled in the Customer's method so save is called just once for the context)
public static SaveResult SaveEditModel(PhoneEditModel model, byte entityType, int entityId, byte phoneType, bool saveChanges) {
SaveResult output = new SaveResult();
try {
if (model != null) {
Phone phone = null;
if (model.PhoneId != 0) {
phone = DataHelper.Context.Phones.Single(x => x.PhoneId == model.PhoneId);
if (phone == null) {
output.Success = false;
output.ErrorMessages.Add("PhoneNotFound", "Unable to find the requested Phone record to update");
}
}
if (string.IsNullOrEmpty(model.PhoneNumber)) {
if (model.PhoneId != 0 && phone != null) {
DataHelper.Context.Phones.DeleteObject(phone);
if (saveChanges) DataHelper.Context.SaveChanges();
}
} else {
if (model.PhoneId == 0) phone = new Phone();
if (phone != null) {
phone.EntityType = entityType;
phone.EntityId = entityId;
phone.PhoneType = phoneType;
phone.PhoneNumber = model.PhoneNumber;
phone.Extension = model.Extension;
if (model.PhoneId == 0) {
phone.CreatedDate = DateTime.Now;
phone.CreatedBy = SiteLogin.Session.ActiveUser;
DataHelper.Context.Phones.AddObject(phone);
} else {
phone.ModifiedDate = DateTime.Now;
phone.ModifiedBy = SiteLogin.Session.ActiveUser;
}
if (saveChanges) DataHelper.Context.SaveChanges();
}
}
}
} catch (Exception exp) {
ErrorHandler.Handle(exp, true);
output.Success = false;
output.ErrorMessages.Add(exp.GetType().ToString(), exp.Message);
output.Exceptions.Add(exp);
}
return output;
}
FYI - SaveResult is just a little container class that I used to get detailed information back if a save fails:
public class SaveResult {
private bool _success = true;
public bool Success {
get { return _success; }
set { _success = value; }
}
private Dictionary<string, string> _errorMessages = new Dictionary<string, string>();
public Dictionary<string, string> ErrorMessages {
get { return _errorMessages; }
set { _errorMessages = value; }
}
private List<Exception> _exceptions = new List<Exception>();
public List<Exception> Exceptions {
get { return _exceptions; }
set { _exceptions = value; }
}
}
The other piece to this is the re-usable UI code for the Phone, Address, etc - which handles all the validation, etc in just one location too.
So, let your thoughts flow and thanks for taking the time to read/review this huge post!

Categories

Resources