Stop a thread working with DB - c#

I'm working on an autocomplete script.
When user stop typing, his input is send to the server which will look for some matches in DB.
Eg: If looking for people "Obam" should return "Barack Obama".
I want this search to be limited to ~500ms. If it takes longer, I want to abort the search and only return results found in time.
I start by looking for a perfect match (this one won't be interrupted), then I am looking for a partial match (this one can be interrupted) :
private static MySqlConnection conn = new MySqlConnection(ConfigurationManager.AppSettings["CxMySql"].ToString());
protected void Page_Load(object sender, EventArgs e)
{
conn.Open();
SearchTerm(table,term,domaine);
conn.Close(); // TimeoutException HERE
conn.Dispose();
}
private void SearchTerm(string table,string term,string domaine)
{
Dictionary<int, Scoring> results = new Dictionary<int, Scoring>();
var requete = "SELECT m.id, m.nom as label FROM marque m WHERE m.nom = '" + term + "'";
var Cmd = new MySqlCommand(requete, conn);
using (var Rd = Cmd.ExecuteReader())
{
while (Rd.Read())
{
results.Add(int.Parse(Rd["id"].ToString()), new Scoring()
{
score = 1000,
value = Rd["label"].ToString()
});
}
}
// Here it should be 500, but I put 1 to force troubles to appear.
RunWithTimeout(() => FindOtherBrands(term, ref results), TimeSpan.FromMilliseconds(1));
var resultsList = results.ToList();
resultsList.Sort(
delegate(KeyValuePair<int, Scoring> firstPair,
KeyValuePair<int, Scoring> nextPair)
{
return nextPair.Value.score - firstPair.Value.score;
}
);
}
private void FindOtherBrands(string term, ref Dictionary<int, Scoring> results)
{
MySqlCommand Cmd;
string requete;
requete = "SELECT m.id, m.nom as label FROM marque m WHERE m.nom LIKE '" + term + "%'";
Cmd = new MySqlCommand(requete, conn);
var Rd = Cmd.ExecuteReader(); // NullReferenceException HERE
while (Rd != null && Rd.Read())
{
int id = int.Parse(Rd["id"].ToString());
if (!results.ContainsKey(id))
{
results.Add(id, new Scoring()
{
score = 100,
value = Rd["label"].ToString()
});
}
}
Rd.Close();
requete = "SELECT m.id, m.nom as label FROM marque m WHERE m.nom LIKE '%" + term + "%'";
Cmd = new MySqlCommand(requete, conn);
Rd = Cmd.ExecuteReader();
while (Rd != null && Rd.Read())
{
int id = int.Parse(Rd["id"].ToString());
if (!results.ContainsKey(id))
{
results.Add(id, new Scoring()
{
score = 10,
value = Rd["label"].ToString()
});
}
}
Rd.Close();
}
I found the RunWithTimeout method here : stop executing code in thread after 30s
bool RunWithTimeout(ThreadStart threadStart, TimeSpan timeout)
{
Thread workerThread = new Thread(threadStart);
workerThread.Start();
bool finished = true;
if (!workerThread.Join(timeout))
{
workerThread.Abort();
finished = false;
}
return finished;
}
Scoring is a struct for an easy sorting of results
private struct Scoring
{
public string value;
public int score;
}
The aim is to have results (not necessarily all) fast.
PROBLEMS
I randomly get Timeoutexception after ~30s on the conn.Close(); line.
I randomly get NullReferenceException on the first Cmd.ExecuteReader(); call in FindOtherBrands.
Can anyone explain me why ?
Am I doing something wrong or is there a workaround ?
I guess the TimeoutException is because I am trying to close connection during command execution, can I drop/cancel that query ?

I would take a different approach. Since you're querying a database, which is naturally asynchronous, you can use async-await for querying the data. With that, you can pass a CancellationToken which is set to a timeout, which you'll monitor with every read:
For example:
private async Task FindOtherBrands(string term,
Dictionary<int, Scoring> results,
CancellationToken cancellationToken)
{
MySqlCommand cmd;
string requete;
requete = "SELECT m.id, m.nom as label
FROM marque m
WHERE m.nom LIKE '" + term + "%'";
cmd = new MySqlCommand(requete, conn);
var Rd = await cmd.ExecuteReaderAsync();
while (Rd != null && await Rd.ReadAsync())
{
cancellationToken.ThrowIfCancellationRequested();
int id = int.Parse(Rd["id"].ToString());
if (!results.ContainsKey(id))
{
results.Add(id, new Scoring()
{
score = 100,
value = Rd["label"].ToString()
});
}
}
Rd.Close();
requete = "SELECT m.id, m.nom as label
FROM marque m
WHERE m.nom LIKE '%" + term + "%'";
cmd = new MySqlCommand(requete, conn);
Rd = await Cmd.ExecuteReaderAsync();
while (Rd != null && await Rd.ReadAsync())
{
cancellationToken.ThrowIfCancellationRequest();
int id = int.Parse(Rd["id"].ToString());
if (!results.ContainsKey(id))
{
results.Add(id, new Scoring()
{
score = 10,
value = Rd["label"].ToString()
});
}
}
Rd.Close();
}
And when you invoke it, all you need is to wrap it in a try-catch and pass a CancellationToken:
private async Task<bool> RunWithTimeoutAsync(TimeSpan timeout)
{
bool finished;
try
{
var cancellationTokenSource = new CancellationTokenSource(timeout);
await FindOtherBrandsAsnyc(term,
results,
cancellationTokenSource.CancellationToken);
finished = true;
}
catch (OperationCanceledException e)
{
// Handle
}
return finished;
}
Side note - Your query is prone to SQL Injection. You shouldn't used string concatenation. Use query parameters instead.

Related

SQLDataReader bugging while using .Nest() or .Read()

I'm retrieving some information from an MSSQL via SQLDataReader, but while debugging it I notice in some cases the reader clears the result view with the error "Enumeration yielded no results" see the screenshot Before Running passing Read(),
After passing read()
this is my code,the error happens on getActiveUsers() method.
getDatabases() works just fine. could someone help me? cheers
public partial class automation : System.Web.UI.Page
{
SqlConnection con;
static List<ActiveUsers> activeUsers = new List<ActiveUsers>();
protected void Page_Load(object sender, EventArgs e)
{
ASPxGridView1.DataSource = activeUsers.ToList();
}
public List<ActiveUsers> getDatabases()
{
//passing query
string SqlQuery = "SELECT [WorkspaceName],[MaConfig_Customers].Name FROM [MaConfig_CustomerDatabases] INNER JOIN [MaConfig_Customers] ON [MaConfig_CustomerDatabases].CustomerId = [MaConfig_Customers].CustomerId where [MaConfig_Customers].Status = 0";
//creating connection
string sqlconn = ConfigurationManager.ConnectionStrings["MaxLiveConnectionString"].ConnectionString;
con = new System.Data.SqlClient.SqlConnection(sqlconn);
var cmd = new SqlCommand(SqlQuery, con);
con.Open();
SqlDataReader reader = cmd.ExecuteReader();
List<ActiveUsers> results = new List<ActiveUsers>();
if (reader.Read())
{
while (reader.Read())
{
ActiveUsers company = new ActiveUsers();
company.DatabaseName = String.Format("{0}", reader["WorkspaceName"]);
company.ClientName = String.Format("{0}", reader["Name"]);
results.Add(company);
}
}
con.Close();
return results;
}
public void getActiveUsers()
{
activeUsers.Clear();
List<ActiveUsers> Databases= getDatabases();
SqlConnection conn = new SqlConnection();
string SqlQuery = "select [disabled], [ADMN_Users1].[Record_Id] ,[ADMN_Users].[User_Id] from admn_Users1 inner join [ADMN_Users] on [ADMN_Users1].[record_Id] = [ADMN_Users].[Record_Id] Where [disabled] & 0x2 = 0 ";
for (int i = 0;i < Databases.Count;i++)
{
conn.ConnectionString =
"Data Source=MAXSQLCLUS01;" +
"Initial Catalog=" + Databases[i].ToString()+";"+
"User id=sa;" +
"Password=Max1m1zer;";
var cmd = new SqlCommand(SqlQuery, conn);
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
int NumberOfUsersCounter = 0 ;
//TODO Select Enabled users
if (reader.Read())
{
while (reader.Read())
{
string user = String.Format("{0}", reader["User_Id"]);
//logic to remove system users
if (user.Equals("master", StringComparison.CurrentCultureIgnoreCase))
{
}
else
if (user.Equals("emailuser", StringComparison.CurrentCultureIgnoreCase))
{
}
else
if (user.Equals("webuser", StringComparison.CurrentCultureIgnoreCase))
{
}
else
{
NumberOfUsersCounter++;
}
}
ActiveUsers newEntry = new ActiveUsers();
newEntry.NumberActiveUsers = NumberOfUsersCounter.ToString();
newEntry.DatabaseName = Databases[i].DatabaseName.ToString();
newEntry.ClientName = Databases[i].ClientName.ToString();
activeUsers.Add(newEntry);
}
conn.Close();
//Add to ActiveUsers list
}
ASPxGridView1.AutoGenerateColumns = true;
ASPxGridView1.DataSource = activeUsers.ToList();
ASPxGridView1.DataBind();
}
protected void ASPxButton1_Click(object sender, EventArgs e)
{
getActiveUsers();
}
protected void btnExportExcel_Click(object sender, EventArgs e)
{
ASPxGridView1.DataBind();
ASPxGridViewExporter1.Landscape = true;
ASPxGridViewExporter1.FileName = "User Count Report";
ASPxGridViewExporter1.WriteXlsToResponse();
}
}
}
if (reader.Read())
{
while (reader.Read())
{
ActiveUsers company = new ActiveUsers();
company.DatabaseName = String.Format("{0}", reader["WorkspaceName"]);
company.ClientName = String.Format("{0}", reader["Name"]);
results.Add(company);
}
}
use this
if (reader.HasRows)
{
while (reader.Read())
{
ActiveUsers company = new ActiveUsers();
company.DatabaseName = String.Format("{0}", reader["WorkspaceName"]);
company.ClientName = String.Format("{0}", reader["Name"]);
results.Add(company);
}
}
your if Condition is wrong
if(reader.Read()) ==> is Wrong
Read() is not return boolean Value
use HasRows to check rows in SQLDataReader
You are skipping the first result. if (reader.Read()) { while(reader.Read()) {.... Remove the enclosing if, all it does is see if there is a row and retrieve it but then you do not read it, instead you do it again in the if so the first result is always discarded.
public List<ActiveUsers> getDatabases()
{
//passing query
string SqlQuery = "SELECT [WorkspaceName],[MaConfig_Customers].Name FROM [MaConfig_CustomerDatabases] INNER JOIN [MaConfig_Customers] ON [MaConfig_CustomerDatabases].CustomerId = [MaConfig_Customers].CustomerId where [MaConfig_Customers].Status = 0";
//creating connection
string sqlconn = ConfigurationManager.ConnectionStrings["MaxLiveConnectionString"].ConnectionString;
using(con = new System.Data.SqlClient.SqlConnection(sqlconn))
using(var cmd = new SqlCommand(SqlQuery, con))
{
con.Open();
using(SqlDataReader reader = cmd.ExecuteReader())
{
List<ActiveUsers> results = new List<ActiveUsers>();
while (reader.Read())
{
ActiveUsers company = new ActiveUsers();
company.DatabaseName = reader.GetString(0);
company.ClientName = reader.GetString(1);
results.Add(company);
}
}
}
return results;
}
Side notes:
company.DatabaseName = String.Format("{0}", reader["WorkspaceName"]) would be better written as company.DatabaseName = reader.GetString(0). The same goes for the next line. No need to use string.Format and you are specifying the columns and order in the query so use the ordinal index so get the native value.
I would recommend you wrap the SqlConnection and SqlDataReader in using blocks to ensure they are closed/disposed after use even in the event of an exception.

assistance with optimization

not sure if stackoverflow is normally used for stuff like this but i'm looking for assistance in optimizing my code. I am running a method to query a count from a database which is fast and works fine. and a timer that is set to run this every 5 minutes. When i implement the timer in my main window after initialization it freezes when opening the window for 30+ seconds. It didn't do that before I added the timers. Any help optimizing this or tips on improving the way this works would be great!
Method that runs SQL query:
public string SQLDataTotalCalls()
{
DateTime dte = DateTime.Today;
string fixedStartDate = String.Format("{0:yyyy-MM-dd " + "05:00:00.000" + "}", dte);
string fixedEndDate = String.Format("{0:yyyy-MM-dd " + "05:00:00.000" + "}", dte.AddDays(1));
SqlConnection connection = null;
using (connection)
{
try
{
var dataSet = new DataSet();
connection = new SqlConnection("Data Source=QL1OADB1;Initial Catalog=OADB;User Id=;Password=");
connection.Open();
var command = new SqlCommand("SELECT COUNT(SOURCEID) AS 'MYCOUNT' "
+ "FROM [OADB].[oadb].[CmsCallHistory] "
+ "WHERE disposition = 2 and DISPSPLIT in (" + SkillNumber + ") AND SEGSTOP BETWEEN '" + fixedStartDate + "' and '" + fixedEndDate + "'",
connection)
{
CommandType = CommandType.Text
};
var dataAdapter = new SqlDataAdapter { SelectCommand = command };
dataAdapter.Fill(dataSet);
return dataSet.Tables[0].Rows[0]["MYCOUNT"].ToString();
}
catch (Exception e)
{
throw new Exception(e.Message, e);
}
finally
{
if (connection != null)
{
connection.Close();
}
}
}
}
Timer that triggers the method every 5 minutes:
public async Task RunPeriodicQueryTotalCalls()
{
TimeSpan interval = TimeSpan.FromMinutes(5);
while (true)
{
await Task.Delay(interval);
string result = await Task.Run((Func<string>)SQLDataTotalCalls);
TotalDailyCalls = result;
}
}
This is where i initialize the window. please note that I run these querys also when the application first launches before the timer runs otherwise the numbers will not post until it runs 5 minutes later:
public ScoreBoardBigViewWindow()
{
InitializeComponent();
string value = "Enter Skill Number Here";
if (Tmp.InputBox("New document", "New document name:", ref value) == System.Windows.Forms.DialogResult.OK)
{
SkillNumber = value;
}
TotalDailyLast7Days = SQLDataSevenPastCalls();
TotalDailyCalls = SQLDataTotalCalls();
TotalDailyAbandon = SQLDataAbandonCalls();
RunPeriodicQueryTotalCalls();
RunPeriodicQueryAbandonCalls();
}
If i remove RunPeriodicQueryTotalCalls(); and RunPeriodicQueryAbandonCalls(); then the performance issue goes away.
If stackoverflow is not the right place to ask this type of question please let me know and I'll reach out to other avenues for further assistance.

SQLite Database is Locked when running two specific unit tests

I'm at a complete loss here. I have two tests: Update_Task and Get_All_Tasks. When run individually, they pass. If I run them together in Visual Studio 2015 (not in parallel -- just selecting them both and clicking run) Update_Task passes and Get_All_Tasks does not. For the latter, I get an error message saying that the database is locked. This implies that Update_Task locks the database and fails to release the lock, but I can't figure out where that would do it.
Any thoughts? Code follows.
EDIT: I found the answer.
It turns out that when you do a var reader = cmd.ExecuteReader(), that reader needs to be put in a using block itself. Otherwise, the reader may not close and that caused the lock. Doing this fixed the problem:
using (var reader = cmd.ExecuteReader())
{
// The query will return only one item, so read it into a new task.
reader.Read();
return this.GetTaskFromReader(reader);
}
Test Methods
[TestMethod]
public void Update_Task()
{
var dbMan = new DatabaseManager();
// Add a new task
var t = new Task()
{
IsEnabled = true,
Description = "Test",
DueDate = DateTime.Now,
Priority = Priority.High
};
dbMan.InsertTask(t);
// Alter the task and update it
t.IsEnabled = false;
t.Description = "Updated test";
t.DueDate = DateTime.Now.AddDays(1);
t.Priority = Priority.Medium;
var updatedTask = this.UpdateTaskAndRetrieve(dbMan, t);
Assert.AreEqual(t, updatedTask, "Task did not update correctly.");
}
[TestMethod]
public void Get_All_Tasks()
{
var dbMan = new DatabaseManager();
// Get the number of tasks currently in the database
var originalCount = dbMan.GetAllTasks().Count;
// Add 5 tasks with different values
for (int i = 0; i < 5; i++)
{
var t = new Task()
{
IsEnabled = i % 2 == 0,
Description = $"Task {i + 1}",
DueDate = DateTime.Now.AddDays(i),
Priority = (Priority)(i % 4)
};
dbMan.InsertTask(t); // <-- Fails when i == 0
}
// Get all the tasks in the database.
var result = dbMan.GetAllTasks();
Assert.AreEqual(originalCount + 5, result.Count, "Wrong number of tasks came back from the database.");
}
private Task UpdateTaskAndRetrieve(DatabaseManager dbMan, Task t)
{
dbMan.UpdateTask(t);
return dbMan.GetTask(t.Id);
}
DatabaseManager Methods
public void UpdateTask(Task task)
{
if (task.Id == 0)
{
throw new InvalidOperationException("Cannot update a task that has not yet been inserted.");
}
using (var connection = new SQLiteConnection(_connectionString))
{
connection.Open();
using (var cmd = new SQLiteCommand(connection))
{
var sql = #"UPDATE [Task]
SET
[enabled]=#enabled,
[description]=#description,
[duedate]=#duedate,
[priority]=#priority
WHERE rowid=#rowId";
// Build up the command and its parameters
cmd.CommandType = CommandType.Text;
cmd.CommandText = sql;
cmd.Parameters.Add(new SQLiteParameter("#enabled", task.IsEnabled ? 1 : 0));
cmd.Parameters.Add(new SQLiteParameter("#description", task.Description));
cmd.Parameters.Add(new SQLiteParameter("#duedate", task.DueDate.Ticks));
cmd.Parameters.Add(new SQLiteParameter("#priority", (int)task.Priority));
cmd.Parameters.Add(new SQLiteParameter("#rowid", task.Id));
var result = cmd.ExecuteNonQuery();
Debug.WriteLine($"Command executed. {result} rows affected.");
}
}
}
public IList<Task> GetAllTasks()
{
using (var connection = new SQLiteConnection(_connectionString))
{
connection.Open();
using (var cmd = new SQLiteCommand(connection))
{
// Create the command
var sql = "SELECT rowid, * from [Task] ORDER BY [duedate] DESC, [priority] DESC";
cmd.CommandText = sql;
// Execute the command and build the resultant list
var reader = cmd.ExecuteReader();
var retList = new List<Task>();
while (reader.Read())
{
retList.Add(this.GetTaskFromReader(reader));
}
return retList;
}
}
}
public Task GetTask(int id)
{
using (var connection = new SQLiteConnection(_connectionString))
{
connection.Open();
using (var cmd = new SQLiteCommand(connection))
{
var sql = "SELECT rowid, * from [Task] WHERE rowid=#id";
cmd.CommandText = sql;
cmd.Parameters.Add(new SQLiteParameter("#id", id));
var reader = cmd.ExecuteReader();
// The query will return only one item, so read it into a new task.
reader.Read();
var task = this.GetTaskFromReader(reader);
return task;
}
}
}
private Task GetTaskFromReader(SQLiteDataReader reader)
{
var retId = Convert.ToInt32(reader["rowid"]);
var desc = reader["description"].ToString();
var date = new DateTime((long)reader["duedate"]);
var isEnabled = Convert.ToBoolean(reader["enabled"]);
var priority = (Priority)Enum.Parse(typeof(Priority), reader["priority"].ToString());
return new Task(retId, isEnabled, desc, date, priority);
}

checking user name or user email already exists

I am working in a simple registration page where the user can't enter the same user name or email, I made a code that prevent the user from entering the username and it worked but when I tried to prevent the user from entring the same username or email it didn't work.
and my question is, "How can I add another condition where the user can't enter email that already exists?"
I tried to do it in this code, but it did't work:
protected void Button_Click(object sender, EventArgs e)
{
SqlConnection con = new SqlConnection( ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString );
SqlCommand cmd1 = new SqlCommand("select 1 from Table where Name =#UserName", con);
SqlCommand cmd2 = new SqlCommand("select 1 from Table where Email=#UserEmail", con);
con.Open();
cmd1.Parameters.AddWithValue("#UserName", Name_id.Text);
cmd2.Parameters.AddWithValue("#UserEmail", Email_id.Text);
using (var dr1 = cmd1.ExecuteReader())
{
if (dr1.HasRows)
{
Label1.Text = "user name already exists";
}
using (var dr2 = cmd2.ExecuteReader())
{
if (dr2.HasRows)
{
Label1.Text = "email already exists";
}
else
{
dr1.Close();
dr2.Close();
//add new users
con.Close();
}
}
}
}
but i get this error:
There is already an open DataReader associated with this Command which must be closed first.
Like I said in my comment your design is bad !
First you should have Data Access Layer. This should be project in big solutions but in your case you can put it like new directory. In this directory you create SqlManager class here is the code:
public class SqlManager
{
public static string ConnectionString
{
get
{
return ConfigurationManager.ConnectionStrings["DevConnString"].ConnectionString;
}
}
public static SqlConnection GetSqlConnection(SqlCommand cmd)
{
if (cmd.Connection == null)
{
SqlConnection conn = new SqlConnection(ConnectionString);
conn.Open();
cmd.Connection = conn;
return conn;
}
return cmd.Connection;
}
public static int ExecuteNonQuery(SqlCommand cmd)
{
SqlConnection conn = GetSqlConnection(cmd);
try
{
return cmd.ExecuteNonQuery();
}
catch
{
throw;
}
finally
{
conn.Close();
}
}
public static object ExecuteScalar(SqlCommand cmd)
{
SqlConnection conn = GetSqlConnection(cmd);
try
{
return cmd.ExecuteScalar();
}
catch
{
throw;
}
finally
{
conn.Close();
}
}
public static DataSet GetDataSet(SqlCommand cmd)
{
return GetDataSet(cmd, "Table");
}
public static DataSet GetDataSet(SqlCommand cmd, string defaultTable)
{
SqlConnection conn = GetSqlConnection(cmd);
try
{
DataSet resultDst = new DataSet();
using (SqlDataAdapter adapter = new SqlDataAdapter(cmd))
{
adapter.Fill(resultDst, defaultTable);
}
return resultDst;
}
catch
{
throw;
}
finally
{
conn.Close();
}
}
public static DataRow GetDataRow(SqlCommand cmd)
{
return GetDataRow(cmd, "Table");
}
public static DataRow GetDataRow(SqlCommand cmd, string defaultTable)
{
SqlConnection conn = GetSqlConnection(cmd);
try
{
DataSet resultDst = new DataSet();
using (SqlDataAdapter adapter = new SqlDataAdapter(cmd))
{
adapter.Fill(resultDst, defaultTable);
}
if (resultDst.Tables.Count > 0 && resultDst.Tables[0].Rows.Count > 0)
{
return resultDst.Tables[0].Rows[0];
}
else
{
return null;
}
}
catch
{
throw;
}
finally
{
conn.Close();
}
}
}
After that you should have Business Object Layer. In bigger solution is project in your case directory. If you are in the page TaxesEdit.aspx, you should add Tax.cs class in the BO(business object).
Example of methods for the class, for your first button:
public DataSet GetTaxesByUserName(string userName)
{
SqlCommand cmd = new SqlCommand(#"
select 1 from Table where Name =#UserName");
cmd.Parameters.AddWithValue("#UserName", userName);
return DA.SqlManager.GetDataSet(cmd);
}
You fetch all the needed data in datasets. After that you make checks like taxesDst.Tables[0].Rows.Count > 0 (or == 0)
For Insert you can have method like this:
public virtual void Insert(params object[] colValues)
{
if (colValues == null || colValues.Length % 2 != 0)
throw new ArgumentException("Invalid column values passed in. Expects pairs (ColumnName, ColumnValue).");
SqlCommand cmd = new SqlCommand("INSERT INTO " + TableName + " ( {0} ) VALUES ( {1} )");
string insertCols = string.Empty;
string insertParams = string.Empty;
for (int i = 0; i < colValues.Length; i += 2)
{
string separator = ", ";
if (i == colValues.Length - 2)
separator = "";
string param = "#P" + i;
insertCols += colValues[i] + separator;
insertParams += param + separator;
cmd.Parameters.AddWithValue(param, colValues[i + 1]);
}
cmd.CommandText = string.Format(cmd.CommandText, insertCols, insertParams);
DA.SqlManager.ExecuteNonQuery(cmd);
}
For this you need to have property TableName in the current BO class.
In this case this methods can be used everywhere and you need only one line of code to invoke them and no problems like yours will happen.
You have opened another DataReader inside the First and thats causing the problem. Here I have re-arranged your code a bit
SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);
SqlCommand cmd1 = new SqlCommand("select 1 from Table where Name =#UserName", con),
cmd2 = new SqlCommand("select 1 from Table where Email=#UserEmail", con);
con.Open();
cmd1.Parameters.AddWithValue("#UserName", Name_id.Text);
cmd2.Parameters.AddWithValue("#UserEmail", Email_id.Text);
bool userExists = false, mailExists = false;
using (var dr1 = cmd1.ExecuteReader())
if (userExists = dr1.HasRows) Label1.Text = "user name already exists";
using (var dr2 = cmd2.ExecuteReader())
if (mailExists = dr2.HasRows) Label1.Text = "email already exists";
if (!(userExists || mailExists)) {
// can add User
}
You need to close one datareader before opening the other one. Although it's not how I'd do it, but you can deal with the runtime error by closing the datareader after each IF:
using (var dr1 = cmd1.ExecuteReader())
{
if (dr1.HasRows)
{
string Text = "user name already exists";
}
dr1.Close();
}
using (var dr2 = cmd2.ExecuteReader())
{
if (dr2.HasRows)
{
string ext = "email already exists";
}
else
{
//add new users
}
dr2.Close();
}
con.Close();
This may work, although there are a few things I would do differently...
protected void Button_Click(object sender, EventArgs e)
{
bool inputIsValid = true;
var con = new SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);
var userNameCmd = new SqlCommand("SELECT 1 FROM Table WHERE Name = #UserName", con);
var emailCmd = new SqlCommand("SELECT 1 FROM Table WHERE Email = #UserEmail", con);
con.Open();
userNameCmd.Parameters.AddWithValue("#UserName", Name_id.Text);
emailCmd.Parameters.AddWithValue("#UserEmail", Email_id.Text);
using (var userNameReader = userNameCmd.ExecuteReader())
{
if (userNameReader.HasRows)
{
inputIsValid = false;
Label1.Text = "User name already exists";
}
}
using (var emailReader = emailCmd.ExecuteReader())
{
if (emailReader.HasRows)
{
inputIsValid = false;
Label1.Text = "Email address already exists";
}
}
if (inputIsValid)
{
// Insert code goes here
}
con.Close();
}
Why don't you do something like this:
[Flags]
public enum ValidationStatus
{
Valid = 0 ,
UserNameInUse = 1 ,
EmailInUse = 2 ,
}
public ValidationStatus ValidateUser( string userName , string emailAddr )
{
ValidationStatus status ;
string connectionString = ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString ;
using ( SqlConnection con = new SqlConnection( connectionString ) )
using ( SqlCommand cmd = con.CreateCommand() )
{
cmd.CommandText + #"
select status = coalesce( ( select 1 from dbo.myTable t where t.UserName = #UserName ) , 0 )
+ coalesce( ( select 2 from dbo.myTable t where t.UserEmail = #UserEmail ) , 0 )
" ;
cmd.Parameters.AddWithValue( "#UserName" , userName ) ;
cmd.Parameters.AddWithValue( "#emailAddr" , emailAddr ) ;
int value = (int) cmd.ExecuteScalar() ;
status = (ValidationStatus) value ;
}
return status ;
}
Aside from anything else, hitting the DB twice for something like this is silly. And this more clearly expresses intent.
Then you can use it in your button click handler, something like this:
protected void Button_Click( object sender , EventArgs e )
{
string userName = Name_id.Text ;
string emailAddr = Email_id.Text ;
ValidationStatus status = ValidateUser( userName , emailAddr ) ;
switch ( status )
{
case ValidationStatus.Valid :
Label1.Text = "" ;
break ;
case ValidationStatus.EmailInUse :
Label1.Text = "Email address in use" ;
break ;
case ValidationStatus.UserNameInUse :
Label1.Text = "User name in use" ;
break ;
case ValidationStatus.EmailInUse|ValidationStatus.UserNameInUse:
Label1.Text = "Both user name and email address in use." ;
break ;
default :
throw new InvalidOperationException() ;
}
if ( status == ValidationStatus.Valid )
{
CreateNewUser() ;
}
}

Is it safe to access SQL Server CE from multiple threads?

I have a large CSV database of about 5MB with ZIP codes, cities, and states that I'm trying to import into a SQL Server CE database.
Using a single thread, the process is estimated to take about 3 hours to complete. While this is fine for getting the job done, I'd like to try and split up the task across multiple threads to cut down on the 3 hours total time. If I create a SqlCeConnection object on each thread, is it safe to run commands on each thread simultaneously?
I have a feeling that there would be issues with concurrency and deadlocks. Here is where I found the CSV database: http://www.unitedstateszipcodes.org/zip-code-database/
Here is my relevant code:
List<AddressSet> addressList;
public void OpenCSV(string file)
{
var addresses = from line in File.ReadAllLines(file).Skip(1)
let columns = line.Split(',')
select new AddressSet
{
ZipCode = columns[0].Replace("\"", "").Trim(),
City = columns[2].Replace("\"", "").Trim(),
State = columns[5].Replace("\"", "").Trim()
};
addressList = addresses.ToList();
Thread worker = new Thread(new ThreadStart(ProcessData));
worker.Start();
}
private void ProcessData()
{
try
{
int i = 1;
DateTime operationStart = DateTime.Now;
foreach (AddressSet address in addressList)
{
int stateId = InsertState(address.State);
int zipCodeId = InsertZipCode(address.ZipCode, stateId);
int cityId = InsertCity(address.City, stateId);
UpdateRelationships(zipCodeId, cityId);
float pct = i / (float)addressList.Count() * 100;
TimeSpan timeSinceStart = DateTime.Now.Subtract(operationStart);
TimeSpan totalTime = TimeSpan.FromMilliseconds(timeSinceStart.TotalMilliseconds / (pct/100));
TimeSpan timeLeft = totalTime - timeSinceStart;
//richTextBox1.BeginInvoke((MethodInvoker)(() => richTextBox1.Text = pct.ToString("N2") + "% (" + i + " of " + addressList.Count().ToString() + ") " + address.City + ", " + address.State + " " + address.ZipCode
// + "\nEstimated Total Time: " + totalTime.Days.ToString() + " days, " + totalTime.Hours.ToString() + " hours, " + totalTime.Minutes.ToString() + " minutes" +
// " - Time Left: " + timeLeft.Days.ToString() + " days, " + timeLeft.Hours.ToString() + " hours, " + timeLeft.Minutes.ToString() + " minutes"));
richTextBox1.BeginInvoke((MethodInvoker)(() => richTextBox1.Text = pct.ToString("N2") + "% (" + i + " of " + addressList.Count().ToString() + ") " + address.City + ", " + address.State + " " + address.ZipCode
+ "\nEstimated Total Time: " + totalTime.ToString("h'h 'm'm 's's'") +
"\nTime Left: " + timeLeft.ToString("h'h 'm'm 's's'") +
"\nRunning Time: " + timeSinceStart.ToString("h'h 'm'm 's's'")));
richTextBox1.BeginInvoke((MethodInvoker)(() => richTextBox1.SelectionStart = richTextBox1.Text.Length));
richTextBox1.BeginInvoke((MethodInvoker)(() => richTextBox1.ScrollToCaret()));
i++;
}
this.Invoke(new Action(() =>
{
MessageBox.Show("Done!");
btnChooseCSV.Enabled = true;
}));
}
catch (Exception ex)
{
this.Invoke(new Action(() =>
{
MessageBox.Show(ex.Message);
}));
}
}
private int InsertZipCode(string zipCode, int stateId)
{
string connstr = System.Configuration.ConfigurationManager.ConnectionStrings["AddressInformation"].ConnectionString;
SqlCeConnection connection = new SqlCeConnection(connstr);
connection.Open();
SqlCeCommand command = new SqlCeCommand("SELECT COUNT(*) FROM ZipCode WHERE ZipCode = #ZipCode", connection);
command.Parameters.AddWithValue("ZipCode", zipCode);
int result = (int)command.ExecuteScalar();
// if nothing found, insert
if (result == 0)
{
command = new SqlCeCommand("INSERT INTO ZipCode(ZipCode, StateId) VALUES(#ZipCode, #StateId)", connection);
command.Parameters.AddWithValue("ZipCode", zipCode);
command.Parameters.AddWithValue("StateId", stateId);
command.ExecuteNonQuery();
command = new SqlCeCommand("SELECT ##IDENTITY", connection);
}
if (result == 1)
{
command = new SqlCeCommand("SELECT ZipCodeId FROM ZipCode WHERE ZipCode = #ZipCode", connection);
command.Parameters.AddWithValue("ZipCode", zipCode);
}
string test = command.ExecuteScalar().ToString();
result = int.Parse(test);
connection.Close();
return result;
}
private int InsertCity(string city, int stateId)
{
string connstr = System.Configuration.ConfigurationManager.ConnectionStrings["AddressInformation"].ConnectionString;
SqlCeConnection connection = new SqlCeConnection(connstr);
connection.Open();
SqlCeCommand command = new SqlCeCommand("SELECT COUNT(*) FROM City WHERE CityName = #City", connection);
command.Parameters.AddWithValue("City", city);
int result = (int)command.ExecuteScalar();
// if nothing found, insert
if (result == 0)
{
command = new SqlCeCommand("INSERT INTO City(CityName, StateId) VALUES(#City, #StateId)", connection);
command.Parameters.AddWithValue("City", city);
command.Parameters.AddWithValue("StateId", stateId);
command.ExecuteNonQuery();
command = new SqlCeCommand("SELECT ##IDENTITY", connection);
}
if (result == 1)
{
command = new SqlCeCommand("SELECT CityId FROM City WHERE CityName = #City", connection);
command.Parameters.AddWithValue("City", city);
}
string test = command.ExecuteScalar().ToString();
result = int.Parse(test);
connection.Close();
return result;
}
private int InsertState(string state)
{
string connstr = System.Configuration.ConfigurationManager.ConnectionStrings["AddressInformation"].ConnectionString;
SqlCeConnection connection = new SqlCeConnection(connstr);
connection.Open();
SqlCeCommand command = new SqlCeCommand("SELECT COUNT(*) FROM State WHERE State = #State", connection);
command.Parameters.AddWithValue("State", state);
int result = (int)command.ExecuteScalar();
// if nothing found, insert
if (result == 0)
{
command = new SqlCeCommand("INSERT INTO State(State) VALUES(#State)", connection);
command.Parameters.AddWithValue("State", state);
command.ExecuteNonQuery();
command = new SqlCeCommand("SELECT ##IDENTITY", connection);
}
if (result == 1)
{
command = new SqlCeCommand("SELECT StateId FROM State WHERE State = #State", connection);
command.Parameters.AddWithValue("State", state);
}
string test = command.ExecuteScalar().ToString();
result = int.Parse(test);
connection.Close();
return result;
}
private void UpdateRelationships(int zipCodeId, int cityId)
{
string connstr = System.Configuration.ConfigurationManager.ConnectionStrings["AddressInformation"].ConnectionString;
SqlCeConnection connection = new SqlCeConnection(connstr);
connection.Open();
SqlCeCommand command = new SqlCeCommand("INSERT INTO CityZipCode(CityId, ZipCodeId) VALUES(#CityId, #ZipCodeId)", connection);
command.Parameters.AddWithValue("CityId", cityId);
command.Parameters.AddWithValue("ZipCodeId", zipCodeId);
command.ExecuteNonQuery();
connection.Close();
}
Edit:
Just to clarify, I'm not just simply inserting each row of information from the CSV file. I'm changing how the data is laid out by inserting each respective item into separate tables and adding relationships between each entity.
For example, a city can have multiple zip codes and a zip code can sometimes cover multiple cities so that would be represented by a many to many relationship. Cities and zip codes have only one state so that relationship is many to one.
I have a table for cities, zip codes, and states. I also have a table for relating cities to zip codes. I will need to modify my relationship table schema to take into effect that cities with the same name may exist in multiple states. The relationship table should really be a set including the city, state and zip code and not just the city and zip code.
My end goal is to distribute the SQL Server CE database with password protection with another application for city, state and zip code validation. I don't want to distribute the CSV database as anyone could change that to pass validation.
You must create a connection object per thread, it is not safe for multithreading:
SqlCeConnection Class
Edited
SQL CE objects are not thread-safe and are not thread affinitive
either. If an instance of SqlCeConnection or SqlCeTransaction is
shared across threads without ensuring thread safety, then that may
lead to Access Violation exception.
It is recommended that each thread should use a separate connection
than sharing across. If there is really a need for sharing SQL CE
objects across threads, then the application should serialize access
to these objects.
Multithreaded programming with SQL Server Compact
Why you don't use SQL Server Compact Toolbox You can use it, which generates INSERT statements based on a CSV file.
or use Conversion of CSV to SQLCE database app
Just a suggestion, I am doing the same kind of thing, and this is what I have, it is extremely fast compared to the simple solution
public static DataTable CSVToDataTable(string path, string name)
{
return CSVToDataTable(Path.Combine(path, name));
}
public static DataTable CSVToDataTable(string path)
{
DataTable res = new DataTable();
if (!File.Exists(path))
{
return res;
}
using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (StreamReader re = new StreamReader(stream))
{
if (re.EndOfStream)
return res;
string line = re.ReadLine();
if (line.IsNullOrWhiteSpace())
return res;
string[] headers = LineToArray(line);
foreach (string header in headers)
{
res.Columns.Add(header);
}
int i = 0;
string[] cells = new string[0];
DataRow row = null;
while (!re.EndOfStream)
{
line = re.ReadLine();
if (line.IsNullOrWhiteSpace())
continue;
cells = LineToArray(line);
row = res.NewRow();
for (i = 0; i < headers.Length && i < cells.Length; i += 1)
{
row[i] = cells[i];
}
res.Rows.Add(row);
}
}
}
return res;
}
private static string[] LineToArray(string line, char delimiter = ',')
{
if (line.Contains("\""))
{
List<string> l = new List<string>();
bool inq = false;
string cell = string.Empty;
char lastCh = 'x';
foreach (char ch in line)
{
if (ch == '"')
{
if (cell.Length == 0)
{
inq = true;
}
else if (lastCh == '\\')
{
cell += ch;
}
else
{
inq = false;
}
}
else if (delimiter == ch)
{
if (inq)
{
cell += ch;
}
else
{
l.Add(cell);
inq = false;
cell = string.Empty;
}
}
else
{
cell += ch;
}
if (inq)
lastCh = ch;
else
lastCh = 'x';
}
return l.ToArray();
}
else
{
return line.Split(new String[] { delimiter.ToString() }, StringSplitOptions.None);
}
}
public void insert(string path, string name, string table, bool KeepNulls){
DataTable data = CSVToDataTable(path, name);
//do data manipulation here
SqlCeBulkCopyOptions options = new SqlCeBulkCopyOptions();
if (KeepNulls)
{
options = options |= SqlCeBulkCopyOptions.KeepNulls;
}
using (SqlCeBulkCopy bc = new SqlCeBulkCopy(Fastway_Remote_Agent.Properties.Settings.Default.DatabaseConnectionString, options))
{
bc.DestinationTableName = table;
bc.WriteToServer(data);
}
}
Using this library: http://sqlcebulkcopy.codeplex.com/
Also for thread pooling (Change it to meet your needs):
/// <summary>
/// Manages open connections on a per-thread basis
/// </summary>
public abstract class SqlCeConnectionPool
{
private static Dictionary<int, DBCon> threadConnectionMap = new Dictionary<int, DBCon>();
private static Dictionary<int, Thread> threadMap = new Dictionary<int, Thread>();
/// <summary>
/// The connection map
/// </summary>
public static Dictionary<int, DBCon> ThreadConnectionMap
{
get { return SqlCeConnectionPool.threadConnectionMap; }
}
/// <summary>
/// Gets the connection string.
/// </summary>
/// <value>The connection string.</value>
public static ConnectionString ConnectionString
{
get { return global::ConnectionString.Default; }
}
/// <summary>
/// Gets a connection for this thread, maintains one open one of each.
/// </summary>
/// <remarks>Don't do this with anything but SQL compact edition or you'll run out of connections - compact edition is not
/// connection pooling friendly and unloads itself too often otherwise so that is why this class exists</remarks>
/// <returns>An open connection</returns>
public static DBCon Connection
{
get
{
lock (threadConnectionMap)
{
//do some quick maintenance on existing connections (closing those that have no thread)
List<int> removeItems = new List<int>();
foreach (var kvp in threadConnectionMap)
{
if (threadMap.ContainsKey(kvp.Key))
{
if (!threadMap[kvp.Key].IsAlive)
{
//close the connection
if (!kvp.Value.Disposed)
kvp.Value.Dispose();
removeItems.Add(kvp.Key);
}
}
else
{
if (!kvp.Value.Disposed)
kvp.Value.Dispose();
removeItems.Add(kvp.Key);
}
}
foreach (int i in removeItems)
{
threadMap.Remove(i);
threadConnectionMap.Remove(i);
}
//now issue the appropriate connection for our current thread
int threadId = Thread.CurrentThread.ManagedThreadId;
DBCon connection = null;
if (threadConnectionMap.ContainsKey(threadId))
{
connection = threadConnectionMap[threadId];
if (connection.Disposed)
{
if (threadConnectionMap.ContainsKey(threadId))
threadConnectionMap.Remove(threadId);
if (threadMap.ContainsKey(threadId))
threadMap.Remove(threadId);
connection = null;
}
else if (connection.Connection.State == ConnectionState.Broken)
{
connection.Dispose();
if (threadConnectionMap.ContainsKey(threadId))
threadConnectionMap.Remove(threadId);
if (threadMap.ContainsKey(threadId))
threadMap.Remove(threadId);
connection = null;
}
else if (connection.Connection.State == ConnectionState.Closed)
{
connection.Dispose();
if (threadConnectionMap.ContainsKey(threadId))
threadConnectionMap.Remove(threadId);
if (threadMap.ContainsKey(threadId))
threadMap.Remove(threadId);
connection = null;
}
}
if (connection == null)
{
connection = new DBCon(ConnectionString);
//connection.Connection.Open();
if (threadConnectionMap.ContainsKey(threadId))
threadConnectionMap[threadId] = connection;
else
threadConnectionMap.Add(threadId, connection);
if (threadMap.ContainsKey(threadId))
threadMap[threadId] = Thread.CurrentThread;
else
threadMap.Add(threadId, Thread.CurrentThread);
}
return connection;
}
}
}
}

Categories