Linq2Sql: changing "delete" extensibility method to update a record - c#

I am writing a CRUD using winforms, connecting to MS SqlServer 2008 via Linq2Sql.
When the user of my application wants to delete a record in the database I dont't want it physically deleted. I just want to set a column called "DlDat" to the current datetime and then update the record instead of deleting it. To force this behaviour in Linq I extend the Delete(table) method autogenerated by sqlmetal.
If e.g. I have a database table called "Unit" and a datacontext called "PdpDataContext" the code looks like this:
public partial class PdpDataContext
{
public PdpDataContext() : base()
{
OnCreated();
}
partial void DeleteUnit(Unit instance)
{
instance.DLDAT = DateTime.Now;
this.ExecuteDynamicUpdate(instance);
}
partial void UpdateUnit(Unit instance)
{
instance.CHDAT = DateTime.Now;
this.ExecuteDynamicUpdate(instance);
}
}
But when I do the delete:
PdpDataContext cx = new PdpDataContext();
cx.ObjectTrackingEnabled = true;
Unit u = new Unit();
u = cx.Unit.Single(x => x.INTUNITNO == 1);
cx.Unit.DeleteOnSubmit(u);
cx.SubmitChanges();
I get an SqlException "wrong syntax near WHERE". Logging the SQL output of the context I see that Linq tries to do an "empty" update:
UPDATE [dbo].[Unit]
SET
WHERE ([INTUNITNO] = #p0) AND ([version] = #p1)
This may be because I use ExecuteDynamicUpdate when the ChangeSet of the context contains only a "delete" and no "update".
I could work around this by just updating the record instead of deleting it when the user presses the delete button or pherhaps by using a stored procedure. But since I am new to the world of Linq I wonder if there is another way by using the datacontext.

The problem with doing it that way is, linq assumes that you are going to use the same CRUD operation that you are overriding. Take a look at this article (Responsibilities of the Developer In Overriding Default Behavior (LINQ to SQL))
This is what I would do:
partial void DeleteUnit(Unit instance)
{
//instance.DLDAT = DateTime.Now;
//this.ExecuteDynamicUpdate(instance);
PdpDataContext cx = new PdpDataContext();
cx.ObjectTrackingEnabled = true;
Unit u = new Unit();
u = cx.Unit.Single(x => x.INTUNITNO == 1);
u.DLDAT = DateTime.Now;
cx.SubmitChanges();
}

I wonder if it wouldn't be better to use an "INSTEAD OF" trigger (delete) on the table... or just don't lie to LINQ: if you want to do an UPDATE (not a DELETE), then update the object (don't delete it). For example, write a method on the data-context that simulates delete, rather than using DeleteOnSubmit.
You may also be able to do something with overriding SubmitChanges and investigating the deltas, but I'm not sure it is a good idea

Related

ASP.NET C#: Entity updating is being blocked

Experiencing an issue about updating mysql DB through EF. It's not the first time I'm dealing with it, so I had some ideas about why isn't my data getting changed. I tried changing an element in goods array; tried editing an object, recieved through LINQ-request (seen some examples of this method); made some attempts on marking element found in the database before editing (like EntityState and Attach()). Nothing of these made any difference, so I tried removing <asp:UpdatePanel> from Site.Master to see what happens (responsive for postback blocking to prevent page shaking on update), but nothing changed (while btnRedeemEdit.IsPostBack having its default value).
Code below is the function I use for updates.
protected void btnRedeemEdit_Click(object sender, EventArgs e)
{
if (!string.IsNullOrEmpty(Request.QueryString["id"]))
{
var db = new GoodContext();
var goods = db.Goods.ToList();
Good theGood = goods.FirstOrDefault(x => x.Id == int.Parse(Request.QueryString["id"]));
//db.Goods.Attach(theGood);//No effect
//db.Entry(theGood).State = System.Data.Entity.EntityState.Modified; //No effect
if (theGood != default)
{
theGood.AmountSold = GetInput().AmountSold;
theGood.APF = GetInput().APF;
theGood.Barcode = GetInput().Barcode;
theGood.Description = GetInput().Description;
theGood.ImagesUrl = GetInput().ImagesUrl;//"https://i.pinimg.com/564x/2d/b7/d8/2db7d8c53b818ce838ad8bf6a4768c71.jpg";
theGood.Name = GetInput().Name;
theGood.OrderPrice = GetInput().OrderPrice;
theGood.Profit = GetInput().Profit;
theGood.RecievedOn = GetInput().RecievedOn;//DateTime.Parse(GetInput().RecievedOn).Date.ToString();
theGood.TotalAmount = GetInput().TotalAmount;
theGood.WeightKg = GetInput().WeightKg;
//SetGoodValues(goods[editIndex],GetInput());//Non-working
db.SaveChanges();
Response.Redirect("/AdminGoods");
}
else Response.Write($"<script>alert('Good on ID does not exist');</script>");
}
else Response.Write($"<script>alert('Unable to change: element selected does not exist');</script>");
}
Notice, that no alerts appear during execution, so object in database can be found.
Are there any more things, that can be responsible for blocking database updates?
A few things to update & check:
Firstly, DbContexts should always be disposed, so in your case wrap the DbContext inside a using statement:
using (var db = new GoodContext())
{
// ...
}
Next, there is no need to load all goods from the DbContext, just use Linq to retrieve the one you want to update:
using (var db = new GoodContext())
{
Good theGood = db.Goods.SingleOrDefault(x => x.Id == int.Parse(Request.QueryString["id"]));
if (theGood is null)
{
Response.Write($"<script>alert('Good on ID does not exist');</script>");
return;
}
}
The plausible suspect is what does "GetInput()" actually do, and have you confirmed that it actually has the changes you want? If GetInput is a method that returns an object containing your changes then it only needs to be called once rather than each time you set a property:
(Inside the using() {} scope...)
var input = GetInput();
theGood.AmountSold = input.AmountSold;
theGood.APF = input.APF;
theGood.Barcode = input.Barcode;
theGood.Description = input.Description;
// ...
db.SaveChanges();
If input has updated values but after calling SaveChanges you aren't seeing updated values in the database then there are two things to check.
1) Check that the database connection string at runtime matches the database that you are checking against. The easiest way to do that is to get the connection string from the DbContext instance's Database.
EF 6:
using (var db = new GoodContext())
{
var connectionString = db.Database.Connection.ConnectionString; // Breakpoint here and inspect.
EF Core: (5/6)
using (var db = new GoodContext())
{
var connectionString = db.Database.GetConnectionString();
Often at runtime the DbContext will be initialized with a connection string from a web.config / .exe.config file that you don't expect so you're checking one database expecting changes while the application is using a different database / server. (More common than you'd expect:)
2) Check that you aren't disabling tracking proxies. By default EF will enable change tracking which is how it knows if/when data has changed for SaveChanges to generate SQL statements. Sometimes developers will encounter performance issues and start looking for ways to speed up EF including disabling change tracking on the DbContext. (A fine option for read-only systems, but a pain for read-write)
EF6 & EF Core: (DbContext initialization)
Configuration.AutoDetectChangesEnabled = false; // If you have this set to false consider removing it.
If you must disable change tracking then you have to explicitly set the EntityState of the entity to Modified before calling SaveChanges():
db.Entry(theGood).State = EntityState.Modified;
db.SaveChanges();
Using change tracking is preferable to using EntityState because with change tracking EF will only generate an UPDATE statement if any values have changed, and only for the values that changed. With EntityState.Modified EF will always generate an UPDATE statement for all non-key fields regardless if any of them had actually changed or not.

DbUpdateConcurrencyException: Entities may have been modified or deleted since entities were loaded

i´m using EF6.
After i clear a tabel and i want to add a new entry to that table i get the following error:
Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded
The code for deleting the databasetable:
public void ResetStatistics() {
_lisDatabase.Database.ExecuteSqlCommand("TRUNCATE TABLE Sortedtubes");
_lisDatabase.sortedtubes.Local.Clear();
}
After that i want to add a new entry to that table with the following code:
Sortedtubes tube = new Sortedtubes();
tube.Time = time;
tube.Milliseconds = time.Millisecond.ToString();
tube.Barcode = barcode;
tube.Tubetype = tubeType;
tube.Target = target;
tube.Materialcode = materialCode;
try {
_lisDatabase.sortedtubes.Add(tube);
_lisDatabase.SaveChanges(); // Error appears here
} catch (DbUpdateConcurrencyException ex) {
// maybe do something here?
}
I tryed the sugestions on the EF documentation with no luck:
https://msdn.microsoft.com/en-us/data/jj592904
EDIT
The problem seems to be the Clear() method (_lisDatabase.sortedtubes.Local.Clear();). After i execute this method the error appears after the next SaveChanges().
So maybe there is an other way to handle this? I have a GridView in my application witch is bind to the sortedtubes entity and i clear it so that the GridView is also cleared, when i truncat the table.
It seems to me that your problem lies in trying to truncate the table and manually clearing your local version of the DbSet. You should just change your EF entities and save them, then those changes are reflected in the database.
This should work:
_lisDatabase.SortedTubes.RemoveRange(_lisDatabase.SortedTubes);
_lisDatabase.SortedTubes.Add(new SortedTube());
_lisDatabase.SaveChanges();
Alternatively, try this:
https://stackoverflow.com/a/10450893/5392513
I suspected that you retrieve the entiries from database with same context by tracking before Truncate the table and when you apply the SaveChanges the EF is trying to delete already deleted records by Truncate. So, usual way to perform it creating a new instance from context and apply actions. It is not good idea to make long database processes with same context. So, in your case if you want to truncate table and use the same context to add new entries later you should detach all tracked entries after truncate action.
public void ResetStatistics()
{
_lisDatabase.Database.ExecuteSqlCommand("TRUNCATE TABLE Sortedtubes");
_lisDatabase.sortedtubes.Local.Clear();
foreach (var entry in _lisDatabase.ChangeTracker.Entries<Sortedtubes>())
{
_lisDatabase.Entry(entry).State = EntityState.Detached;
}
}
Also, if it is possible use .AsNoTracking in your queries before Truncate the table and no need to detach entries manually.
So.. finaly i think i got a solution.
My reset method looks like this now:
public void ResetStatistics() {
_lisDatabase.sortedtubes.RemoveRange(_lisDatabase.sortedtubes);
_lisDatabase.SaveChanges();
}
And my add stays the same:
_lisDatabase.sortedtubes.Add(tube);
_lisDatabase.SaveChanges();
but the "big" change was in my Sortedtubes entity. I changed the type of Time from DateTime to a normal String
from:
[Key]
[Column(Order = 1)]
public DateTime? Time { get; set; }
to:
[StringLength(45)]
public string Time { get; set; }
The problem seems to be the Type of the Time property of the Sortedtubes Entity. But i don`t know why.
If anyone wants to explain me this case?
Thanks to all for your help.

Updating Database via Anonymous Type?

The following code gets all the rows from my Activities table that have not already been posted on Twitter. It then loops through and posts Twitter updates for each of those row. In the process, I would like to update the database to indicate these rows have now been "twittered".
However, I'm getting an error (indicated below) when I try and update this value. I assume this is because I'm using an anonymous type. However, if I use the full type, that will require pulling a lot of unnecessary data from the database.
Is there a way to accomplish this efficiently? Or is this yet another case where EF forces me to make compromises in performance?
using (MyEntities context = new MyEntities())
{
var activities = from act in context.Activities
where act.ActTwittered == false
select new { act.ActID, act.ActTitle, act.Category,
act.ActDateTime, act.Location, act.ActTwittered };
foreach (var activity in activities)
{
twitter.PostUpdate("...");
activity.ActTwittered = true; // <== Error: ActTwittered is read-only
}
}
You could try a "fake object approach" like this:
using (MyEntities context = new MyEntities())
{
var activities = from act in context.Activities
where act.ActTwittered == false
select new { act.ActID, act.ActTitle, act.Category,
act.ActDateTime, act.Location, act.ActTwittered };
foreach (var activity in activities)
{
twitter.PostUpdate("...");
// Create fake object with necessary primary key
var act = new Activity()
{
ActID = activity.ActID,
ActTwittered = false
};
// Attach to context -> act is in state "Unchanged"
// but change-tracked now
context.Activities.Attach(act);
// Change a property -> act is in state "Modified" now
act.ActTwittered = true;
}
// all act are sent to server with sql-update statements
// only for the ActTwittered column
context.SaveChanges();
}
It's "theoretical" code, not sure if it would work.
Edit
Not "theoretical" anymore. I've tested this with DbContext of EF 4.1 and it works as described in the sample code above. (Because DbContext is only a wrapper API around ObjectContext it's almost safe to assume that it also will work in EF 4.0.)
If you simply select 'act', then it should work. Don't forget to submit after editing.
Why are you calling select new instead of returning entire object. Entity framework will only be able to update property if it is correctly defined in schema resources which certainly is not case with anonymous type.
Entity framework will never be able to determine which table and which field the property is mapped to.

LINQ: SubmitChanges() not updating my record

Not to sound like a broken record here (there a few posts that look like this one) but none of them seem to solve my problem. It seems that when you want to update
private bool resetPassword(string password)
{
try
{
var db = new SchedulerDBDataContext();
// since this is a instance method, I grab the ID from _this_
AdminUser user = db.AdminUsers.SingleOrDefault(t => t.ID == _ID);
if (user != null)
{
// this method DOES update these two fields.
SchedUtil.md5Hash(password, ref user._EncryptedPassword, ref user._PasswordSalt);
// I threw these in there to try something... it didn't work.
//user._EncryptedPassword = user.EncryptedPassword;
//user._PasswordSalt = user.PasswordSalt;
// this DOESN'T do anything.
db.SubmitChanges();
return true;
}
return false;
}
catch (Exception)
{
return false;
}
}
Maybe this a dumb question but I'm retrieving this from the db... why not just update this's properties. I'm guess I need to pull it through the DBContext I guess.
You should be setting the public properties and not the private values.
// I threw these in there to try something... it didn't work.
//user._EncryptedPassword = user.EncryptedPassword;
//user._PasswordSalt = user.PasswordSalt;
This won't trigger any updates.
Even if you do :
user.EncryptedPassword = user._EncryptedPassword;
user.PasswordSalt = user._PasswordSalt;
this won't trigger any change either as you are not actually changing the values
You can do something like
string newEncryptedPassword;
string newPasswordSalt;
SchedUtil.md5Hash(password, ref newEncryptedPassword, ref newPasswordSalt);
user.EncryptedPassword = newEncryptedPassword;
user.PasswordSalt = newPasswordSalt;
Also check that your table has a primary key, otherwise Linq will not track the changes.
DJ,
Are you sure
user._EncryptedPassword ,
user._PasswordSalt
are the properties ? I think you LINQ TO SQL creates public and private properties.
Can you set them
user.EncryptedPassword ,
user.PasswordSalt
like this ?
Ved
To troubleshoot your code, try any of these suggestions:
while debugging the code, I'll assume that your object is not null.
ensure that your properties are actually changed. It's odd that you're using pipe-prefixed field names, but either way: while debugging, check that your properties actually have new values.
use SQL Server Profiler to capture the SQL statement sent to the database server. You'll then be able to re-run this UPDATE query back into SQL Management Studio, and determine how many records are effected. You'll also be able to see the values passed in the UPDATE statement.
Ved pointed out one possible problem. Just in case that doesn't work, you should double check your LINQ to SQL class' AdminUser class definition and make sure that the generated code implements the INotifyPropertyChanging and INotifyPropertyChanged interfaces. There are some cases where the designer will not implement these interfaces which prevents updates from working. e.g., not declaring a primary key.

LINQ to SQL: To Attach or Not To Attach

So I'm have a really hard time figuring out when I should be attaching to an object and when I shouldn't be attaching to an object. First thing's first, here is a small diagram of my (very simplified) object model.
In my DAL I create a new DataContext every time I do a data-related operation. Say, for instance, I want to save a new user. In my business layer I create a new user.
var user = new User();
user.FirstName = "Bob";
user.LastName = "Smith";
user.Username = "bob.smith";
user.Password = StringUtilities.EncodePassword("MyPassword123");
user.Organization = someOrganization; // Assume that someOrganization was loaded and it's data context has been garbage collected.
Now I want to go save this user.
var userRepository = new RepositoryFactory.GetRepository<UserRepository>();
userRepository.Save(user);
Neato! Here is my save logic:
public void Save(User user)
{
if (!DataContext.Users.Contains(user))
{
user.Id = Guid.NewGuid();
user.CreatedDate = DateTime.Now;
user.Disabled = false;
//DataContext.Organizations.Attach(user.Organization);
DataContext.Users.InsertOnSubmit(user);
}
else
{
DataContext.Users.Attach(user);
}
DataContext.SubmitChanges();
// Finished here as well.
user.Detach();
}
So, here we are. You'll notice that I comment out the bit where the DataContext attachs to the organization. If I attach to the organization I get the following exception:
NotSupportedException: An attempt has been made to Attach or Add an
entity that is not new, perhaps having
been loaded from another DataContext.
This is not supported.
Hmm, that doesn't work. Let me try it without attaching (i.e. comment out that line about attaching to the organization).
DuplicateKeyException: Cannot add an entity with a key that is already
in use.
WHAAAAT? I can only assume this is trying to insert a new organization which is obviously false.
So, what's the deal guys? What should I do? What is the proper approach? It seems like L2S makes this quite a bit harder than it should be...
EDIT: I just noticed that if I try to look at the pending change set (dataContext.GetChangeSet()) I get the same NotSupportedException I described earlier!! What the hell, L2S?!
It may not work exactly like this under the hood, but here's the way I conceptualize it: When you summon an object from a DataContext, one of the things Linq does is track the changes to this object over time so it knows what to save back if you submit changes. If you lose this original data context, the Linq object summoned from it doesn't have the history of what has changed in it from the time it was summoned from the DB.
For example:
DbDataContext db = new DbDataContext();
User u = db.Users.Single( u => u.Id == HARD_CODED_GUID );
u.FirstName = "Foo";
db.SubmitChanges();
In this case since the User object was summoned from the data context, it was tracking all the changes to "u" and knows how to submit those changes to the DB.
In your example, you had a User object that had been persisted somewhere (or passed from elsewhere and do not have it's original DataContext it was summoned from). In this case, you must attach it to the new data context.
User u; // User object passed in from somewhere else
DbDataContext db = new DbDataContext();
u.FirstName = "Foo";
DbDataContext.Users.Attach( u );
db.SubmitChanges();
Since the relationship between user and organization is just a GUID (OrganizationId) in your data model, you only have to attach the user object.
I'm not sure about your scaffolding code, but maybe something like this:
private const Guid DEFAULT_ORG = new Guid("3cbb9255-1083-4fc4-8449-27975cb478a5");
public void Save(User user)
{
if (!DataContext.Users.Contains(user))
{
user.Id = Guid.NewGuid();
user.CreatedDate = DateTime.Now;
user.Disabled = false;
user.OrganizationId = DEFAULT_ORG; // make the foreign key connection just
// via a GUID, not by assigning an
// Organization object
DataContext.Users.InsertOnSubmit(user);
}
else
{
DataContext.Users.Attach(user);
}
DataContext.SubmitChanges();
}
So "attach" is used when you take an object that exists from the database, detach it (say by marshalling it over a webservice somewhere else) and want to put it back into the database. Instead of calling .Attach(), call .InsertOnSubmit() instead. You're almost there conceptually, you're just using the wrong method to do what you want.
I used an big table with 400+ columns. No way am I going to map and test all that!
Get the original object from database, and attach it with the amended object. Just make sure the object coming back in is fully populated other wise it will override it the DB with blanks!
Or you can copy the original GET into memory and work on a proper copy (not just reference) of the MOdel, then pass the original and the changed one in, instead of re getting like I do in the example. This is just an example of how it works.
public void Save(User user)
{
if (!DataContext.Users.Contains(user))
{
user.Id = Guid.NewGuid();
user.CreatedDate = DateTime.Now;
user.Disabled = false;
user.OrganizationId = DEFAULT_ORG; // make the foreign key connection just
// via a GUID, not by assigning an
// Organization object
DataContext.Users.InsertOnSubmit(user);
}
else
{
var UserDB = DataContext.Users.FirstOrDefault(db => db.id == user.id); //Costs an extra call but its worth it if oyu have 400 columns!
DataContext.Users.Attach(user, userDB); //Just maps all the changes on the flu
}
DataContext.SubmitChanges();
}

Categories