I have this following code:
if (await TryUpdateModelAsync<Host>(
hostToUpdate,
"Host",
s => s.Name, s => s.Description, s => s.Address, s => s.Postcode, s => s.Suburb,
s => s.State))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Host)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The host was deleted by another user.");
return Page();
}
var dbValues = (Host)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Host.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Host.RowVersion");
}
}
I have a properties for Host called: LastDateModified and LastModified which is calculated/predefined value
ie DateTime.Now for LastDateModified and _userManager.GetUserId(User) for LastDateModifiedBy.
So how do I pass this into this code?
await TryUpdateModelAsync<Host>(
hostToUpdate,
"Host",
s => s.Name, s => s.Description, s => s.Address, s => s.Postcode, s => s.Suburb,
s => s.State)
You can set the (alternative) value prior to saving the object:
var hostToUpdate = await _context.Host.FindAsync(s => s.Id == id);
if (await TryUpdateModelAsync(
hostToUpdate,
"host", // empty with MVC
s => s.Name, s => s.Description, s => s.Address,
s => s.Postcode, s => s.Suburb, s => s.State))
{
try
{
hostToUpdate.LastModified = DateTime.Now;
hostToUpdate.LastDateModifiedBy = _userManager.GetUserId(User);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
// ...
}
Please note that LastModified and LastDateModifiedBy are not part of the TryUpdateModelAsync statement. But if they were, the values will be overwritten by the action.
From the Razor Pages documentation:
The DB context keeps track of whether entities in memory are in sync
with their corresponding rows in the DB. The DB context sync
information determines what happens when SaveChangesAsync is called.
From the Mvc documentation (no longer updated):
The Entity Framework's automatic change tracking sets the Modified
flag on the fields that are changed by form input. When the
SaveChanges method is called, the Entity Framework creates SQL
statements to update the database row.
To explain why this works, first TryUpdateModelAsync updates the fields updated by the user and then the action updates the additional fields. All are tracked and saved by the Entity Framework. This is default Entity Framework behaviour.
As a side note, an alternative for you would be to add code that automatically updates the fields. In that case you can't forget to set them and you save a few lines of code. And you won't have to change your code at all.
The strategy is that the entity implements the base fields and updates them on save changes. Here's a more extended version:
public interface IBaseEntity
{
DateTime LastDateModified { get; set; }
string LastDateModifiedBy { get; set; }
DateTime DateCreated { get; set; }
string DateCreatedBy { get; set; }
}
public class Host : IBaseEntity
{
// the other fields
// ...
public DateTime LastDateModified { get; set; }
public string LastDateModifiedBy { get; set; }
public DateTime DateCreated { get; set; }
public string DateCreatedBy { get; set; }
}
The context:
public partial class MyContext : DbContext
{
// Reference to the name of the current user.
private readonly string _userName;
public MyContext(DbContextOptions<MyContext> options, IHttpContextAccessor httpContext)
: base(options)
{
// Save the name of the current user so MyContext knows
// who it is. The advantage is that you won't need to lookup
// the User on each save changes.
_userName = httpContext.HttpContext.User.Identity.Name;
}
public virtual DbSet<Host> Host { get; set; }
// You'll only need to override this, unless you are
// also using non-async SaveChanges.
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
UpdateEntries();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
// You can move this to another partial class.
private void UpdateEntries()
{
// Modified
var modified = ChangeTracker.Entries().Where(v => v.State == EntityState.Modified && typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
modified.ForEach(entry =>
{
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastDateModifiedBy = _userName;
});
// Added
var added = ChangeTracker.Entries().Where(v => v.State == EntityState.Added && typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
added.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateCreated = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DateCreatedBy = _userName;
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastDateModifiedBy = _userName;
});
}
// ...
}
Add HttpContextAccessor in Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
Now each time an object that implements IBaseEntity is saved, the fields are automatically updated.
Please note that I didn't inject UserManager here. If the User contains a name claim then you can use that instead. This will save a call to the database.
As an improvement you can write a new service that takes care of resolving the user name and inject that instead.
Related
I have created a class, ObjectBuilder - I am injecting my dbContext into the class constructor and setting it to a read only attribute in the constructor method.
The class has a few different methods that use the dbContext, for example 'CreateorUpdateObject.
In my API controller I am instantiating the ObjectBuilder class. I am then trying to call the 'CreateorUpdateObject' method three times in a row. The first time everything works as intended. The second time, I get an ObjectDisposedException. I understand what this means. However I can't work out how to resolve this issue, seemingly executing a single method will always dispose the context - therefore none of the subsequent methods will work. I am not using any using clauses or disposing of the context myself. Can someone explain to me how to prevent dbContext from dispsoing so I can chain methods.
All the best!
Here is a snippet of the ObjectBuilder Class
class ObjectBuilder
{
readonly GuidObjectBuilder _guidObjectBuilder;
readonly projectviewerContext _db;
bool UpdatedGuidsArray { get; set; }
int UpdatedObjectCount { get; set; }
int CreatedObjectCount { get; set; }
ProjectObjects ActiveObject { get; set; }
public ObjectBuilder(GuidObjectBuilder guidObjectBuilder, projectviewerContext db)
{
this._guidObjectBuilder = guidObjectBuilder;
this._db = db;
this.UpdatedGuidsArray = false;
this.UpdatedObjectCount = 0;
this.CreatedObjectCount = 0;
}
public async Task<dynamic> CreateOrUpdateObject(string projectName, string tag, string pidClass, string guId, string fileName)
{
//SEND ONE ENTRY RETURNS A PROJECT_OBJECT MODEL
if (this._db == null) throw new ArgumentNullException("The object builder class has not been set a database. Set the object with a database");
try
{
//Check for an initial entry
List<ProjectObjects> matchingProjectObjects = (from data in this._db.ProjectObjects.ToList()
where data.ProjectName == projectName &&
data.Tag == tag
select data).ToList();
if (matchingProjectObjects.Any())
{
this.ActiveObject = this.UpdateProjectObject(matchingProjectObjects[0], pidClass, guId, fileName);
this._db.Update(this.ActiveObject);
this.UpdatedObjectCount++;
}
else
{
this.ActiveObject = this.CreateProjectObject(projectName, tag, pidClass, guId, fileName);
this._db.Add(this.ActiveObject);
this.CreatedObjectCount++;
}
await this._db.SaveChangesAsync();
if (this.UpdatedGuidsArray)
{
//Add new guid
await this._guidObjectBuilder.CreateOrUpdateUponModelOpen(this.ActiveObject.Id, guId, fileName);
}
this.UpdatedGuidsArray = false;
return this.ActiveObject;
}
catch (System.Exception error)
{
return error;
}
}
}
Here is a snippet of my test endpoint:
[HttpPost]
[Route("api/objectCreation/test1")]
public async Task<dynamic> TestOne()
{
try
{
projectviewerContext db = new projectviewerContext();
ObjectBuilder objectBuilder = new ObjectBuilder(new GuidObjectBuilder(db), db);
await objectBuilder.CreateOrUpdateObject("Project 1", "VV00011", "Gate Valve", "XXCCBBVVNNDDFFHH", "3D_Model.nwd");
await objectBuilder.CreateOrUpdateObject("Project 1", "VV00012", "Gate Valve", "XXFFGGHHIILLMMNN", "3D_Model.nwd");
await objectBuilder.CreateOrUpdateObject("Project 1", "VV00014", "Gate Valve", "123456789", "PID_drawing.nwd");
return "bang";
}
catch (System.Exception error)
{
return error.Message;
}
}
I am working on a .NET 6 Winforms project that uses Entity Framework Core 6.I can retrieve data OK, but when I go to Edit data and call SaveChanges on the DbContext, it just hangs with no error. The UI freezes and when debugging, the code never steps past the SaveChanges call.
The data is actually updated in the database but it never moves past this point and I have to abort the application.
I enabled logging and I can see the SQL calls being made to EF Core 6 in the log with no errors.
I am wondering if there is some threading issue I am missing where it is not returning back to the UI, but then I would expect the debugger to at least be able to step past the SaveChanges call, but it does not. I have searched the Internet and cannot find any solution.
Here is the DbContext class;
public class MainDbContext : DbContext
{
public MainDbContext(DbContextOptions<MainDbContext> options)
: base(options)
{
}
public DbSet<User> Users => Set<User>();
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var result = await base.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return result;
}
public override int SaveChanges()
{
return SaveChangesAsync().GetAwaiter().GetResult();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new UserConfiguration());
}
}
Here is the User Entity class;
public class User
{
public int Id { get; set; }
public string? CompanyName { get; set; } = string.Empty;
public string? Name { get; set; } = string.Empty;
public bool? Active { get; set; }
}
I am using DI to handle the DbContext. Here is the AddDbContext in Program.cs
Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) =>
{
// Configure the app here
})
.ConfigureServices((hostContext, services) =>
{
var sqlTransientErrors = new List<int>() { 10928, 10929, 10053, 10054, 10060, 40197, 40540, 40613, 40143, 64 };
var MainDbConnectionString = "{CONNECTION STRING OMITTED}";
var commandTimeout = "30";
services.AddTransient<UserTest>();
services.AddDbContext<CdiMainDbContext>(options =>
options.UseSqlServer(
MainDbConnectionString,
sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(5, TimeSpan.FromSeconds(30), sqlTransientErrors);
sqlOptions.CommandTimeout(Convert.ToInt32(commandTimeout));
}));
})
.UseSerilog();
Here is the Click event code for the Update button on the form;
private void UpdateButton_Click(object sender, EventArgs e)
{
int userId = int.Parse(UserIdTextBox.Text.Trim());
var user = _mainDbContext.Users.AsNoTracking().FirstOrDefault(x => x.Id == userId);
if (user != null)
{
user.Name = NameTextBox.Text;
user.CompanyName = CompanyNameTextBox.Text;
user.Active = ActiveCheckBox.Checked;
_mainDbContext.Users.Update(user);
_mainDbContext.SaveChanges();
}
}
Any ideas?
I'm building an n-tier web app in ASP.NET Core 3.1 that uses Entity Framework to interact with a SQL Server database. Everything works fine except for updating an entity twice without refreshing in between both requests.
So the first time I read and entity from the database and update it, it works perfectly. When I try to update the entity immediately after, I get the following exception:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s).
Data may have been modified or deleted since entities were loaded...
I'm using the generic repository pattern in combination with asynchronous code.
An overview of my code:
GenericRepository.cs:
public class Repository<T> : IRepository<T> where T : class
{
protected readonly DataContext _context;
protected DbSet<T> _entities;
public Repository(DataContext context)
{
this._context = context;
_entities = context.Set<T>();
}
}
MemberRepository.cs:
public class MemberRepository : Repository<Member>, IMemberRepository
{
public MemberRepository(DataContext context) : base(context)
{
//
}
public async override Task<Member> SelectByIdAsync(int id)
{
return await this._entities
.Where(m => m.Id == id)
.Include(m => m.Addresses)
.Include(m => m.MemberHobbies)
.Include(m => m.Car)
.Include(m => m.EmailAddresses)
.FirstOrDefaultAsync();
//return await this._context.Members.Where(m => m.Id == id).Include(m => m.Addresses).Include(m => m.MemberHobbies).Include(m => m.Car).Include(m => m.EmailAddresses).FirstOrDefaultAsync();
}
public async override Task UpdateAsync(Member member)
{
this._context.Members.Update(member);
//this._context.Set<Member>().Update(member);
await this._context.SaveChangesAsync();
}
}
startup.cs:
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddScoped<IMemberRepository, MemberRepository>();
I have tried to change the way I read or update my entity (see the commented out lines), but those give the same result. Also: I have other entities which work perfectly, the only difference is that the Member class has multiple foreign keys (see .Include(...))
Might it have anything to do with improper use of asynchronous functions? Maybe because I'm not returning the updated Member and thus next time I execute an update I'm sending a now out of date member?
Also: I found this answer, but it makes no sense to me at all
Thanks!
I use the generic code like this for update:
public virtual async Task<T> UpdateAsync(T t)
{
if (t == null) return null;
T exist;
try
{
exist = await Context.Set<T>().FindAsync(t.Id);
if (exist == null)
{
t.ErrorMessage = "Can't find item to update";
return t;
}
Context.Entry(exist).CurrentValues.SetValues(t);
var result = await Context.SaveChangesAsync();
if (result == 0) t.ErrorMessage = "Can't saved item";
}
catch (Exception ex)
{
t.ErrorMessage = ex.Message;
return t;
}
return exist;
}
Or you can try this code for Member class:
public async Task UpdateAsync(Member member)
{
try
{
var exist = await _context.Members.Where(i.Id=member.Id).FirstOrDefaultAsync();
if (exist == null) ....errorMessage = "Can't find item to update";
Context.Entry(exist).CurrentValues.SetValues(member);
var result = await Context.SaveChangesAsync();
if (result == 0) ....errorMessage = "Can't save item";
}
catch (Exception ex)
{
...errorMessage = ex.Message;
}
}
EF keeps track of your data. Here's what you do:
Get two copies of the same entry;
Change the former copy;
Update the change for former copy;
Change the latter copy, which kept the track of the entry's state prior the 3rd step;
Update the change made by the latter copy, but since this change reflects to the former state of an entry - you get that error;
Here's what you should do instead:
Add an extra layer to your business objects that will carry the states around;
Get a current state of your database entry upon receiving the update request;
Apply the changes to your DB entry;
Save;
Example:
// DB entry
public class Member
{
public int Id { get; set; }
public string Name { get; set; }
}
// DTO for carrying around
public class MemberForNameChangeDto
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Repo
{
public Task<bool> UpdateMemberName(MemberForNameChangeDto memberForNameChange)
{
// _context.Members is DbSet<Member>
var memberEntry = _context.Members.Find(memberForNameChange.Id);
// null checks, verifications, etc
memberEntry.Name = memberForNameChange.Name;
var result = await _context.SaveChangesAsync();
return result > 0;
}
}
MyModel objRecord = new MyModel();
objRecord.ID = 42;
objRecord.Name = "Dave";
_context.CaseComplainants.Add(objRecord);
await _context.SaveChangesAsync();
List<MyOtherModel> lstRecord = new List<MyOtherModel>();
lstRecord = await _context.MyOthers
.Include(p => p.Title)
.Where(n => n.MyOtherID == objRecord.ID)
.ToListAsync();
This code works fine if it's all in the same method. When I move the second chunk into it's own method I get the error message
System.ObjectDisposedException: 'Cannot access a disposed object. A
common cause of this error is disposing a context that was resolved
from dependency injection and then later trying to use the same
context instance elsewhere in your application. This may occur if you
are calling Dispose() on the context, or wrapping the context in a
using statement. If you are using dependency injection, you should let
the dependency injection container take care of disposing context
instances. ObjectDisposed_ObjectName_Name'
The _context is class wide and hovering over it shows what you'd expect.
The model looks like this.
public class MyOtherModel {
[ForeignKey("ID")]
public int ID { get; set; }
[ForeignKey("TitleID")]
public int? TitleID { get; set; }
public virtual LookUp Title { get; set; }
I've looked into the error message and all the problems that I can find are to do with the general set up of the EF infrastructure, one way or another. I can't find one that talks about anything non-infrastructure related.
Any ideas why moving the code into another method breaks it?
Edit
The context is initialised in the boilerplate ConfigureServices, straight out of the example documentation
public void ConfigureServices(IServiceCollection services) {
services.AddDbContext<MyContextModel>(options => options.UseSqlServer(Configuration.GetConnectionString("MyModel")));
This works
public async Task<IActionResult> OnPostAsync({
MyModel objRecord = new MyModel();
objRecord.ID = 42;
objRecord.Name = "Dave";
_context.CaseComplainants.Add(objRecord);
await _context.SaveChangesAsync();
List<MyOtherModel> lstRecord = new List<MyOtherModel>();
lstRecord = await _context.MyOthers
.Include(p => p.Title)
.Where(n => n.MyOtherID == objRecord.ID)
.ToListAsync();
This doesn't
public async Task<IActionResult> OnPostAsync({
MyModel objRecord = new MyModel();
objRecord.ID = 42;
objRecord.Name = "Dave";
_context.CaseComplainants.Add(objRecord);
await _context.SaveChangesAsync();
DoStuff(objRecord.ID);
}
private async void DoStuff(int TheID) {
List<MyOtherModel> lstRecord = new List<MyOtherModel>();
lstRecord = await _context.MyOthers
.Include(p => p.Title)
.Where(n => n.MyOtherID == TheID)
.ToListAsync();
I'm adding a new method to a service that already works. This new method is used by a HangFire job. This is how I add it to the Configure method of the Startup.cs
// Create the daily tasks
RecurringJob.AddOrUpdate<ITaskService>(x => x.CreateRecurringTasks(), Cron.Daily(0));
And this is the constructor of the service. Note that I create the DB context in the start so I don't have transaction problems when using it inside a controller.
public TaskService(
IMapper mapper,
INotificationService notificationService,
IConfiguration configuration
)
{
var opts = new DbContextOptionsBuilder<ProjectDbContext>();
opts.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
_dbContext = new ProjectDbContext(opts.Options);
_configuration = configuration;
_mapper = mapper;
_notificationService = notificationService;
}
My problem is that the method below won't add the row in the database.
void LogRepeatedTask(long copiedTaskId, long originalTaskId) {
_dbContext.TaskRepeatLogs.Add(new Data.Models.TaskRepeatLog
{
CopiedTaskId = copiedTaskId,
OriginalTaskId = originalTaskId,
UtcDate = DateTime.UtcNow
});
_dbContext.SaveChanges();
}
This is the model. As you can see it is pretty simple:
public class TaskRepeatLog
{
public long Id { get; set; }
public long OriginalTaskId { get; set; }
public long CopiedTaskId { get; set; }
public DateTime UtcDate { get; set; }
}
And this is the DbSet in the ProjectDbContext:
public DbSet<TaskRepeatLog> TaskRepeatLogs { get; set; }
Everything seems pretty straightforward to me but I can't understand why the method is not working. Even the select on this DbSet seems not to work properly and I don't understand what I did wrong. Can you help me?
Thanks
I found the issue. The problem was that I was calling the LogRepeatedTask inside a loop and it was throwing an error saying that there was another transaction happening.
This is where I was calling the function. I just removed from there, added the Ids to a dictionary and I'm using from there.
var tasks = _dbContext.Tasks
.Include(i => i.RepeatConfig)
.Where(p => p.RepeatTask && p.RepeatConfig != null && p.CopiedFromTaskId == null);
if (tasks != null)
{
foreach (var task in tasks)
{
// I was calling the method here.
}
}
So there was no weird behavior.