I would like to encrypt an existing database column with always encrypted. My project is a ASP.NET project using code first and database is SQL Server. The database has already data. I created a migration to achieve my goal.
First I tried to alter the column type, using the following.
ALTER TABLE [dbo].[TestDecrypted] ALTER COLUMN [FloatCol] [float] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK_Auto1], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL
I got the following error.
Operand type clash: float is incompatible with float encrypted with (encryption_type = 'RANDOMIZED', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256', column_encryption_key_name = 'CEK_Auto1', column_encryption_key_database_name = 'TestEncrypt')
Then I decided to created another column and migrate the data.
ALTER TABLE [dbo].[TestDecrypted] ADD [FloatCol2] [float] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK_Auto1], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL
UPDATE [dbo].[TestDecrypted] SET [FloatCol2] = [FloatCol]
And I got the same error.
After I looked at this, I noticed that it is possible to insert data like the following
DECLARE #floatCol FLOAT = 1.1
UPDATE [dbo].[TestDecrypted] SET [FloatCol2] = #floatCol
But if I try to obtain the value from my existing column, it fails.
DECLARE #floatCol FLOAT = (SELECT TOP 1 FloatCol FROM TestDecrypted)
UPDATE [dbo].[TestDecrypted] SET FloatCol2 = #floatCol
The error follows.
Encryption scheme mismatch for columns/variables '#floatCol'. The encryption scheme for the columns/variables is (encryption_type = 'PLAINTEXT') and the expression near line '4' expects it to be (encryption_type = 'RANDOMIZED', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256', column_encryption_key_name = 'CEK_Auto1', column_encryption_key_database_name = 'TestEncrypt').
Does anyone knows how can I achieve my goal?
Update 1
#Nikhil-Vithlani-Microsoft did some interesting suggestions.
Always Encrypted Wizard in SSMS - I would like to achieve my goal with code first migrations, so this idea does not fit.
SqlBulkCopy - It does not work inside migrations, because the new column will only exist after all 'Up' method is run. Therefore we cannot insert data into this column in this way inside this method.
Anyway, his suggestions drove me to another attempt: obtain the decrypted values and update the encrypted column with them.
var values = new Dictionary<Guid, double>();
var connectionString = ConfigurationManager.ConnectionStrings["MainDb"].ConnectionString;
using (var sourceConnection = new SqlConnection(connectionString))
{
var myCommand = new SqlCommand("SELECT * FROM dbo.TestDecrypted", sourceConnection);
sourceConnection.Open();
using (var reader = myCommand.ExecuteReader())
{
while (reader.Read())
{
values.Add((Guid)reader["Id"], (double)reader["FloatCol"]);
}
}
}
Sql("ALTER TABLE [dbo].[TestDecrypted] ADD [FloatCol2] [float] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK_Auto1], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL");
foreach (var valuePair in values)
{
// The error occurs here
Sql($#"DECLARE #value FLOAT = {valuePair.Value}
UPDATE [dbo].[TestDecrypted] SET [FloatCol2] = #value WHERE Id = '{valuePair.Key}'");
}
In fact, I did not try to create another column and to migrate the data, as mentioned in an example above. I tried it only on SSMS.
And now I got a different error.
Transaction (Process ID 57) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
I tried to do it without encrypting the new column, and it worked properly.
Any idea why this error occurs?
You will have to do the always encrypted related migration outside of entity framework. This blog should help
https://blogs.msdn.microsoft.com/sqlsecurity/2015/08/27/using-always-encrypted-with-entity-framework-6/
If you want to encrypt an existing column, you can use Always Encrypted Wizard in SSMS, or use this article that explains how to migrate existing data.
Also, please note that doing bulk inserts through a C# (.NET 4.6.1+ client) app is supported.
You can do this in c# using SqlBulkCopy specifically using SqlBulkCopy.WriteToServer(IDataReader) Method.
Create a new table (encryptedTable) with the same schema as that of your plaintext table (unencryptedTable) but with the encryption turned on for the desired columns.
Do select * from unencryptedTable to load the data in a SqlDataReader then use SqlBulkCopy to load it to the encryptedTable using SqlBulkCopy.WriteToServer(IDataReader) Method
For example,
Plaintext Table
CREATE TABLE [dbo].[Patients](
[PatientId] [int] IDENTITY(1,1),
[SSN] [char](11) NOT NULL)
Encrypted Table
CREATE TABLE [dbo].[Patients](
[PatientId] [int] IDENTITY(1,1),
[SSN] [char](11) COLLATE Latin1_General_BIN2
ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC,
ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',
COLUMN_ENCRYPTION_KEY = CEK1) NOT NULL)
As for why your method does not work,
when you use parameterization for always encrypted, the right hand side (RHS) of the declare statement needs to be a literal. Because the driver will identify the literal and encrypt it for you. So, the following will not work, since RHS is a sql expression and cannot be encrypted by the driver
DECLARE #floatCol FLOAT = (SELECT TOP 1 FloatCol FROM TestDecrypted)
UPDATE [dbo].[TestDecrypted] SET FloatCol2 = #floatCol
Update:
The following code will not work because parameterization for Always Encrypted only applies to SSMS
foreach (var valuePair in values)
{
// The error occurs here
Sql($#"DECLARE #value FLOAT = {valuePair.Value}
UPDATE [dbo].[TestDecrypted] SET [FloatCol2] = #value WHERE Id = '{valuePair.Key}'");
}
However, if you rewrite your code as follows, that should work
foreach (var valuePair in values)
{
SqlCommand cmd = _sqlconn.CreateCommand();
cmd.CommandText = #"UPDATE [dbo].[TestDecrypted] SET [FloatCol2] = #FloatVar WHERE Id = '{valuePair.Key}'");";
SqlParameter paramFloat = cmd.CreateParameter();
paramFloat.ParameterName = #"#FloatVar";
paramFloat.DbType = SqlDbType.Float;
paramFloat.Direction = ParameterDirection.Input;
paramFloat.Value = floatValue;
cmd.Parameters.Add(paramFloat);
cmd.ExecuteNonQuery();
}
Hope that helps, if you have additional question, please leave them in the comments.
Related
Im new to ASP.net webforms.Im having a event page,in which i have a field to add sales channel heads mail id.when i click on the plus button i will be able to add more than one sales channels head.
For inserting the form values into the database im using Stored procedure.and its inserting the records with one sales channel head email id.
I want to know how i can write a stored procedure for inserting dynamic textbox values into sql server for the same record(That is the id and event name should be same).
This is my stored procedure
CREATE PROCEDURE SPInsertEvent
#eventName varchar(200),
#eventDate date,
#costPerHead varchar(200),
#totalCost varchar(200),
#salesChannelHeads varchar(200),
#salesManagers varchar(200),
#eventCreationTime datetime
AS
BEGIN
SET NOCOUNT ON
-- Insert statements for procedure here
INSERT INTO dbo.hp_event
(event_name, event_date, cost_per_head, total_cost, sales_channel_head, sales_manager, event_creation_time)
VALUES
(#eventName, #eventDate, #costPerHead, #totalCost, #salesChannelHeads, #salesManagers, #eventCreationTime)
END
This is my ASP.net function
SqlCommand cmd = new SqlCommand("SPInsertEvent", conn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("EventName", txtEventName.Text);
cmd.Parameters.AddWithValue("EventDate", Convert.ToDateTime(txtEventDate.Text));
cmd.Parameters.AddWithValue("CostPerHead", txtTotCostPerHead.Text);
cmd.Parameters.AddWithValue("TotalCost", txtTotalCostEvent.Text);
cmd.Parameters.AddWithValue("SalesChannelHead", txtSalesChannelHead.Text);
cmd.Parameters.AddWithValue("SalesManager", txtSalesManagers.Text);
cmd.Parameters.AddWithValue("EventCreationTime", DateTime.Now);
conn.Open();
int k = cmd.ExecuteNonQuery();
if (k != 0)
{
string message = "Event added successfully.";
string script = "window.onload = function(){ alert('";
script += message;
script += "')};";
ClientScript.RegisterStartupScript(this.GetType(), "SuccessMessage", script, true);
}
conn.Close();
Instead of storing all the list of email ids for the given event in one table, I would suggest you to store them in separate table and you can reference them from the hp_event table whenever you need. So your database design should be some thing like below where eventid of hp_eventSalesManagers references eventId of hp_event -
To make this design work you can make use of Table Valued Parameters in ADO.NET and follow the below steps:
Create a User Defined Type -
CREATE TYPE [dbo].[ChannelHeads] As Table
(
EmailIds VARCHAR(50)
)
Whenever you click button populate a new Data Table(I am using Session to keep track
of the previous data), below is the sample code:
protected void btnAdd_Click(object sender, EventArgs e)
{
if (Session["DataTable"] == null)
{
dataTable = new DataTable();
dataTable.Columns.Add("EmailIds", typeof(string));
Session.Add("DataTable", dataTable);
}
else
{
//If yes then get it from current session
dataTable = (DataTable)Session["DataTable"];
}
DataRow dt_row;
dt_row = dataTable.NewRow();
dt_row["EmailIds"] = name.Text;
dataTable.Rows.Add(dt_row);
}
When submitting to data base add the below parameter(See the way I am passing the data table to DB):
SqlParameter parameterSalesChannelHeads = new SqlParameter();
parameterSalesChannelHeads.ParameterName = "#salesChannelHeads";
parameterSalesChannelHeads.SqlDbType = System.Data.SqlDbType.Structured;
parameterSalesChannelHeads.Value = (DataTable)Session["DataTable"];
parameterSalesChannelHeads.TypeName = "dbo.ChannelHeads";
cmd.Parameters.Add(parameterSalesChannelHeads);
Change all your parameters in above format just to make sure you are using
Parameters.Add instead of Parameters.AddWithValue as mentioned here
Finally change the procedure as below to populate the tables, below is one of the way,
you can enable error handling and improve the code:
ALTER PROCEDURE SPInsertEvent
#eventName varchar(200),
#eventDate datetime,
#costPerHead varchar(200),
#totalCost varchar(200),
#salesChannelHeads As [dbo].[ChannelHeads] Readonly,
#salesManagers varchar(200),
#eventCreationTime datetime
AS
BEGIN
SET NOCOUNT ON
DECLARE #eventID INT
-- Insert statements for procedure here
INSERT INTO dbo.hp_event
(event_name, eventDate, costPerHead, totalCost, eventCreationTime,
salesManagers)
VALUES
(#eventName, #eventDate, #costPerHead, #totalCost,#eventCreationTime,
#salesManagers)
SET #eventID = SCOPE_IDENTITY()
INSERT INTO dbo.hp_eventSalesManagers
(eventid,event_name,salesChannelHeads)
SELECT #eventID, #eventName, EmailIds
FROM
#salesChannelHeads
END
Finally change the data types of the fields accordingly as mentioned in the comment section for better clarity and usages.
You said in the comments "What i need is a stored procedure for inserting saleschannel heads email id(txtSalesChannelHead,txtSalesChannelHead1,txtSalesChannelHead2) into the sql server table with same id,that is there will be duplicate rows in the table". Handling a dynamic number of inputs like that is not best done in a stored procedure, from what i can see of your scenario. The easier way is to run the insert procedure from your .NET code once for each textbox. Now I don't know how your textboxes are being added, so I can't tell you how to get the set of textbox values, but once you have it, a simple foreach loop will let you run the stored procedure once for each of them.
String SQL = "INSERT INTO Employee (name, age, salary) VALUES (:name,:age,:salary)";
Map namedParameters = new HashMap();
namedParameters.put("name", name);
namedParameters.put("age", age);
namedParameters.put("salary", salary);
namedParameterJdbcTemplate.update(SQL, namedParameters);
String SQL = "UPDATE Employee SET age = :age WHERE empid = :empid";
SqlParameterSource namedParameters = new MapSqlParameterSource();
namedParameters.addValue("age", age);
namedParameters.addValue("empid", empid);
namedParameterJdbcTemplate.update(SQL, namedParameters);
Seems both Map and SqlParameterSource are same. But why did API developers added these API's ? Is there any particular scenario to use Map or SqlParameterSource which makes execution faster? Please explain me clearly. Thanks in advance.
Using a Map is fine for simple cases, but there are two benefits to using SqlParamaterSource over a Map.
The first is simply the builder pattern allowing you to add multiple values inline (namedParameters.addValue().addValue().addValue() etc).
The second is more powerful. The jdbcTemplate will auto-determine the sqlType of your map values while the SqlParamaterSource allows you to explicitly use the sqlType of your choice. This can be an issue depending on your database, indexes and parameters.
An example would be Integers and Longs with an Oracle database. The jdbc template will add these objects to your query with surrounding quotes '' making them effectively strings in your database query. If you have a number in your database with leading 0's it will not be found because '0XXXX' will not match 'XXXX'. If you pass in the right sqlType, the jdbc template will do a number comparison without quotes so XXXX will equal XXXX.
When my place holder values were of different datatypes, this (MapSqlParameterSource) really helped me:
String SQL = "UPDATE Employee SET joindate = :joinDate WHERE empid = :empid";
MapSqlParameterSource namedParameters = new MapSqlParameterSource();
namedParameters.addValue("date", joinDate, Types.Date);
namedParameters.addValue("empid", empid, Types.Integer);
namedParameterJdbcTemplate.update(SQL, namedParameters);
I'm using Firebird 2.5 and asp.net (4.5).
I'm trying to find out how to use insert ... returning, or some equivalent.
Using fbDataReader, it executes the insert OK, but I can't find anyway of accessing a returned value. Using fbDataReader.GetName(0) seems to work ok, returning the variable name in the "returning" clause. This even applies to a max() in a subselect:
..... returning (select max(userid) as newid from users)
returns the text "newid".
I can't find where, or whether, the value is available.
Using a fbDataAdaptor to fill a DataTable, the insert works OK, but data table seems empty.
Does anyone know whether this is possible, and if so, how it's done?
Thanks
EDIT
Code supplied :
strConn = ....
dbConn = New FirebirdSql.Data.FirebirdClient.FbConnection(strConn)
dbConn.Open()
MySQL = "insert into users (Firstname, Lastname) VALUES (#fname,#lname) returning userid"
FbC = New FirebirdSql.Data.FirebirdClient.FbCommand(MySQL, dbConn)
FbC.Parameters.Add("fname", FirebirdSql.Data.FirebirdClient.FbDbType.Text).Value = "Pete"
FbC.Parameters.Add("lname", FirebirdSql.Data.FirebirdClient.FbDbType.Text).Value = "Davis"
FbDataReader = FbC.ExecuteReader()
FbDataReader.Read()
TextBox1.Text = FbDataReader.GetName(0)
'TextBox1.Text = str(FbDataReader.GetInt64())
'TextBox1.Text = FbDataReader.GetString(0)
TextBox1.Text = FbDataReader.GetValue(0)
According to this thread INSERT ... RETURNING ... behaves like output parameters for the Firebird .NET provider. So you will need to add an output parameter.
So something like the code below should work:
FbParameter outParam = new FbParam("userid", FbDbType.Integer)
{
Direction = ParameterDirection.Output
};
FbC.Parameters.Add(outParam);
FbC.ExecuteNonQuery();
int? userId = outParam.Value as int?;
I have a general question on whether something can be done - and whether it will be the most efficient way of doing it !
To summarise: can I pass an object collection as a parameter to a stored procedure?
Let's say that I have a SQL Server table called Users [UserID, Forename, Surname]
and another table called Hobbies [HobbyID, UserID, HobbyName, HobbyTypeID]
This set up is to record multiple hobbies against a user.
In my application, I want to update the user record.
Normally - I would update the user table and then in code, loop through each hobby and update the hobbies table record by record.
If I'm updating the user forename and 2 of their hobbies, this would require 3 calls to the database.
(1 call to a stored procedure to update the forename/surname, and 2 calls to a stored procedure to update the 2 hobby records)
My question is:
Can I make just 1 call to the database by passing all the parameters to just 1 stored procedure.
eg.
intUserID = 1
strForename = "Edward"
strSurname = "ScissorHands"
dim objHobbyCollection as New List(Of Hobby)
'Assume that I have 2 hobby objects, each with their hobbyID, UserID, HobbyName & HobbyTypeID
Dim params As SqlParameter()
params = New SqlParameter() {
New SqlParameter("#UserID", intUserID),
New SqlParameter("#Forename", strForename),
New SqlParameter("#Surname", strSurname),
New SqlParameter("#Hobbies", objHobbyCollection)
}
Can I do this ? (and which way would be more efficient?)
What would the Stored Procedure look like ?
ALTER PROCEDURE [dbo].[User_Update]
#UserID INT
,#Forename NVARCHAR(50) = NULL
,#Surname NVARCHAR(50) = NULL
,#Hobbies ??????????????
Assuming SQL Server 2008+, you can do this using a table-valued parameter. First in SQL Server create a table type:
CREATE TYPE dbo.HobbiesTVP AS TABLE
(
HobbyID INT PRIMARY KEY,
HobbyName NVARCHAR(50),
HobbyTypeID INT
);
Then your stored procedure would say:
#Hobbies dbo.HobbiesTVP READONLY
In C# (sorry I don't know vb.net equivalent) it would be as follows (but if you just have one UserID, this doesn't need to be part of the collection, does it?):
// as Steve pointed out, you may need to have your hobbies in a DataTable.
DataTable HobbyDataTable = new DataTable();
HobbyDataTable.Columns.Add(new DataColumn("HobbyID"));
HobbyDataTable.Columns.Add(new DataColumn("HobbyName"));
HobbyDataTable.Columns.Add(new DataColumn("HobbyTypeID"));
// loop through objHobbyCollection and add the values to the DataTable,
// or just populate this DataTable in the first place
using (connObject)
{
SqlCommand cmd = new SqlCommand("dbo.User_Update", connObject);
cmd.CommandType = CommandType.StoredProcedure;
// other params, e.g. #UserID
SqlParameter tvparam = cmd.Parameters.AddWithValue("#Hobbies", HobbyDataTable);
tvparam.SqlDbType = SqlDbType.Structured;
// ...presumably ExecuteNonQuery()
}
I want to insert a record and then update the record according to scope_identity of inserted record.
I'm doing this but when I want to update my record encounter an error.
WorkshopDataContext Dac = new WorkshopDataContext();
Dac.Connection.ConnectionString = "Data Source=dpsxxx-xxx;Initial Catalog=kar;User ID=sa;Password=xxxx";
Tbl_workshop Workshop = new Tbl_workshop();
Workshop.StateCode = Bodu.BduStateCode;
Workshop.CityCode = Bodu.BduCityCode;
Workshop.Co_workshop=12222;
Dac.Tbl_workshop.InsertOnSubmit(Workshop);
Dac.SubmitChanges();
Int64 Scope = Workshop.id;
var query = from record in Dac.Tbl_workshop where record.id == Scope select record;
query.First().co_Workshop = Scope;
Dac.SubmitChanges();
and this is the error:
Value of member 'co_Workshop' of an object of type 'Tbl_Workshop' changed.
A member defining the identity of the object cannot be changed.
Consider adding a new object with new identity and deleting the existing one instead.
If you have properly configured your Linq-to-SQL model to reflect the IDENTITY column in your table, you should have the new value available right after .SubmitChanges():
Tbl_workshop Workshop = new Tbl_workshop();
Workshop.StateCode = Bodu.BduStateCode;
Workshop.CityCode = Bodu.BduCityCode;
Workshop.Co_workshop=12222;
Dac.Tbl_workshop.InsertOnSubmit(Workshop);
Dac.SubmitChanges();
Int64 workshopID = Workshop.Id; // you should get new ID value here - automatically!!
You don't need to do anything like reading out that new value from SQL Server or anything - Linq-to-SQL should automagically update your Workshop object with the proper value.
Update: to update your co_workshop value to the value given by the IDENTITY ID, do this (just set the value of co_workshop and save again - that's really all there is):
Dac.Tbl_workshop.InsertOnSubmit(Workshop);
Dac.SubmitChanges();
Int64 workshopID = Workshop.Id; // you should get new ID value here - automatically!!
Workshop.Co_workshop = workshopID;
Dac.SubmitChanges();
As it said on the error, you can't change co_Workshop because its identity (auto increment value). To freely edit it, you need to edit the database and remove this setting.
What probably is happening is that both id and co_Workshop are set as identity. Just disable the identity checkbox from co_Workshop.