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?
Related
I got the following error when I try to test an update operation using Entity Framework core:
System.InvalidOperationException : The instance of entity type 'Companies' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
After doing some research, I tried everything that I found:
Create in scope DB context
deattach and attached the object I want to update from the DB context
Return the object to be updated using "AsNoTracking()" , my repository actually do this.
For the testing I am using EF in-memmory database with it fixture, I am using XUnit and .NET 5.
Can I get any help with this please?
Here is my code:
// The repository I am trying to test
public class RepositoryBase<T> : ICrudRepository<T> where T : class, IModel
{
protected PrjDbContext DatabaseContext { get; set; }
public RepositoryBase(PrjDbContext databaseContext) => DatabaseContext = databaseContext;
protected IQueryable<T> FindAll() => DatabaseContext.Set<T>().AsNoTracking();
protected IQueryable<T> FindBy(Expression<Func<T, bool>> expression) => DatabaseContext.Set<T>().Where(expression).AsNoTracking();
public void Create(T entity) => DatabaseContext.Set<T>().Add(entity);
public void Update(T entity) => DatabaseContext.Set<T>().Update(entity);
public void Delete(T entity) => DatabaseContext.Set<T>().Remove(entity);
public async Task<IEnumerable<T>> ReadAllAsync() => await FindAll().ToListAsync().ConfigureAwait(false);
public async Task<T> ReadByIdAsync(int id) => await FindBy(entity => entity.Id.Equals(id)).FirstOrDefaultAsync().ConfigureAwait(false);
}
//The Database context
public partial class PrjDbContext : DbContext
{
public PrjDbContext()
{
}
public PrjDbContext(DbContextOptions<PrjDbContext> options)
: base(options)
{
}
public virtual DbSet<Companies> Companies { get; set; }
}
// This is my fixture with the in-memory Database
public sealed class PrjSeedDataFixture : IDisposable
{
public PrjDbContext DbContext { get; }
public PrjSeedDataFixture(string name)
{
string databaseName = "PrjDatabase_" + name + "_" + DateTime.Now.ToFileTimeUtc();
DbContextOptions<PrjDbContext> options = new DbContextOptionsBuilder<PrjDbContext>()
.UseInMemoryDatabase(databaseName)
.EnableSensitiveDataLogging()
.Options;
DbContext = new PrjDbContext(options);
// Load Companies
DbContext.Companies.Add(new Companies { Id = 1, Name = "Customer 1", Status = 0, Created = DateTime.Now, LogoName = "FakeLogo.jpg", LogoPath = "/LogoPath/SecondFolder/", ModifiedBy = "Admin" });
DbContext.Companies.AsNoTracking();
DbContext.SaveChanges();
}
public void Dispose()
{
DbContext.Dispose();
}
}
The test method "Update_WhenCalled_UpdateACompanyObject", is not working for me.
// And finally, this is my test class, Create_WhenCalled_CreatesNewCompanyObject pass the test, but Update_WhenCalled_UpdateACompanyObject isn't passing the test.
public class RepositoryBaseCompanyTests
{
private Companies _newCompany;
private PrjDbContext _databaseContext;
private RepositoryBase<Companies> _sut;
public RepositoryBaseCompanyTests()
{
_newCompany = new Companies {Id = 2};
_databaseContext = new PrjSeedDataFixture("RepositoryBase").DbContext;
_sut = new RepositoryBase<Companies>(_databaseContext);
}
[Fact]
public void Create_WhenCalled_CreatesNewCompanyObject()
{
//Act
_sut.Create(_newCompany);
_databaseContext.SaveChanges();
//Assert
Assert.Equal(2, _databaseContext.Companies.Where( x => x.Id == 2).FirstOrDefault().Id);
}
[Fact]
public async void Update_WhenCalled_UpdateACompanyObject()
{
//Arrange
var company = await _sut.ReadByIdAsync(1);
company.Name = "Customer 2";
//_databaseContext.Entry(company).State = EntityState.Detached;
//_databaseContext.Attach(company);
//_databaseContext.Entry(company).State = EntityState.Modified;
//Act
_sut.Update(company);
await _databaseContext.SaveChangesAsync();
//Assert
Assert.Equal("Customer 2", _databaseContext.Companies.Where(x => x.Id == 1).FirstOrDefault().Name);
}
}
If you are using EF Core 5.0 then call DbContext.ChangeTracker.Clear() (or go through DbContext.Entries collection and set state to Detached for earlier ones) after DbContext.SaveChanges(); in PrjSeedDataFixture ctor. Adding/Updating an entry makes it tracked and you are reusing the context that created an entry with Id = 1, so when _sut.Update(company); is called it will try to track it again (since ReadByIdAsync should return an untracked one).
P.S.
Adding an extra repository abstraction layer around EF can be considered as antipattern (because EF already implements repository/UoW patterns) and the issue you are having can be one of the examples of why that is true and why this abstraction can be a leaky one. So if you still decide that having one is a good idea - you need to proceed with caution.
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.
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.
I started to use .net core (asp.net core) with the Entity Framework.
I plan to implement a webservice (using asp.net and GraphQL).
now i wonder wheter it is possible use the use the RegularExpression Attribute for validation in the Entity Framework.
But it seems to be ignored.
All samples i found with such attributes was client and server side in C#.
Is this not supposed to work in the EF (serveside) like this ?
Is there an easy way to make this work without to write tons of code ?
PS: im using "Microsoft.EntityFrameworkCore" with PostgreSQL
Why is following Code not throwing an exception if the RegularExpression for Book.something is not meet (ps: it's also not firing if it's meet):
...
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace aspPlainEF
{
public class Startup
{
EFCoreDemoContext ef = new EFCoreDemoContext();
...
public class Book
{
[Key]
public int Id { get; set; }
...
[Required]
[RegularExpression(#"^hello$", ErrorMessage = "You can not have that")]
public string something { get; set; }
}
public class EFCoreDemoContext : DbContext
{
public DbSet<Book> Books { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
optionsBuilder.UseNpgsql(...);
}
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
app.Run(async (context) =>
{
Book book = new Book();
...
book.something = "xxx";
ef.Add(book); ef.SaveChanges();
await context.Response.WriteAsync("hell1");
});
}
}
}
See my Validation in EF Core post for more details.
Here's the gist of it:
public override int SaveChanges()
{
var entities = from e in ChangeTracker.Entries()
where e.State == EntityState.Added
|| e.State == EntityState.Modified
select e.Entity;
foreach (var entity in entities)
{
var validationContext = new ValidationContext(entity);
Validator.ValidateObject(
entity,
validationContext,
validateAllProperties: true);
}
return base.SaveChanges();
}
seems to be a problem in the current dotnetcore version Validation attributes don't work at all ??.
What however works is:
public class Book : IValidatableObject
{
...
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (something != "hello")
{
yield return new ValidationResult("Error message goes here");
}
}
I found a decent article to get me started on unit testing my Entity Framework-based application using Moq: https://msdn.microsoft.com/en-us/data/dn314429.aspx
This issue I'm having is that the SaveChanges method of the Mock does not appear to trigger the ValidateEntity method like it normally would. None of the validation settings I configured in the EntityTypeConfiguration are being thrown as a DbEntityValidationException.
For example, my AddRoles_Fails_For_Empty_Name tests to make sure that the service cannot add a role with an empty name. Either the IsRequired() configuration is not being applied, or the ValidateEntity method is not being called. I should mention that it works correctly if I use the actual context in the web app.
I've included some of my relevant unit testing, DbContext, and Service code below.
Am I doing something incorrectly? Are there any known issues or workarounds?
Role DB Map
public class RoleMap : EntityTypeConfiguration<Role>
{
public RoleMap()
{
ToTable("bm_Roles");
HasKey(r => r.Id);
Property(r => r.Name).IsRequired().HasMaxLength(100).HasIndex(new IndexAttribute("UX_Role_Name") { IsUnique = true });
Property(r => r.Description).HasMaxLength(500);
}
}
DbContext
public class BlueMoonContext : DbContext, IBlueMoonContext
{
public BlueMoonContext() : base("name=BlueMoon")
{
}
public DbSet<Role> Roles { get; set; }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.AddFromAssembly(typeof(BlueMoonContext).Assembly);
}
public void MarkAsModified<T>(T entity) where T : class
{
entity.ThrowIfNull("entity");
Entry<T>(entity).State = EntityState.Modified;
}
protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
var result = base.ValidateEntity(entityEntry, items);
if (entityEntry.State == EntityState.Added || entityEntry.State == EntityState.Modified)
{
// Perform validations that require database lookups
if (entityEntry.Entity is Role)
{
ValidateRole((Role)entityEntry.Entity, result);
}
else if (entityEntry.Entity is User)
{
ValidateUser((User)entityEntry.Entity, result);
}
}
return result;
}
private void ValidateRole(Role role, DbEntityValidationResult result)
{
if (role.Name.HasValue() && !Roles.NameAvailable(role.Name, role.Id))
{
result.ValidationErrors.Add(new DbValidationError("Name", "Already in use"));
}
}
private void ValidateUser(User user, DbEntityValidationResult result)
{
if (user.UserName.HasValue() && !Users.UserNameAvailable(user.UserName, user.Id))
{
result.ValidationErrors.Add(new DbValidationError("UserName", "Already in use"));
}
if (user.Email.HasValue() && !Users.UserNameAvailable(user.UserName, user.Id))
{
result.ValidationErrors.Add(new DbValidationError("Email", "Already in use"));
}
}
}
Account Service
public class AccountService : BaseService, IAccountService
{
private IPasswordHasher _passwordHasher;
public AccountService(IBlueMoonContext context, IPasswordHasher passwordHasher) : base(context)
{
_passwordHasher = passwordHasher;
}
public ServiceResult CreateRole(Role role)
{
role.ThrowIfNull("role");
Context.Roles.Add(role);
return Save();
}
// Copied from base service class
protected ServiceResult Save()
{
var result = new ServiceResult();
try
{
Context.SaveChanges();
}
catch (DbEntityValidationException validationException)
{
foreach (var validationError in validationException.EntityValidationErrors)
{
foreach (var error in validationError.ValidationErrors)
{
result.AddError(error.ErrorMessage, error.PropertyName);
}
}
}
return result;
}
}
Unit Test
[TestFixture]
public class AccountServiceTests : BaseTest
{
protected Mock<MockBlueMoonContext> _context;
private IAccountService _accountService;
[TestFixtureSetUp]
public void Setup()
{
_context = new Mock<BlueMoonContext>();
var data = new List<Role>
{
new Role { Id = 1, Name = "Super Admin" },
new Role { Id = 2, Name = "Catalog Admin" },
new Role { Id = 3, Name = "Order Admin" }
}.AsQueryable();
var roleSet = CreateMockSet<Role>(data);
roleSet.Setup(m => m.Find(It.IsAny<object[]>())).Returns<object[]>(ids => data.FirstOrDefault(d => d.Id == (int)ids[0]));
_context.Setup(m => m.Roles).Returns(roleSet.Object);
// _context.Setup(m => m.SaveChanges()).Returns(0);
_accountService = new AccountService(_context.Object, new CryptoPasswordHasher());
}
[Test]
public void AddRole_Fails_For_Empty_Name()
{
var role = new Role { Id = 4, Name = "" };
var result = _accountService.CreateRole(role);
Assert.False(result.Success);
}
}
SaveChanges is a virtual method which means you invoke a fake method....
You can create your mock CallBase = true, but it is not a good idea(it miss the idea of UT):
_context = new Mock<BlueMoonContext>(){ CallBase = true };
The above code will use the real implementation of BlueMoonContext for any method/property which is not explicitly setup.
RoleMap is responsible for your DB stracture, you should test it as a part of integration test(with DB).
In my opinion you should create an integration tests to verify the integrity(for example; cover RoleMap) with your DB, And create a UT using the Throw setup to cover the catch section(it's a part of your unit):
_contest.Setup(x => x.SaveChanges())
.Throws(new DbEntityValidationException());
Edit to answer the OP question in the comment
no, you don't have to separate the built in validation, you have to create another test(integration test). In this test you'll verify the validation behaviour: insert an illegal entity, expect that exception will raise(using ExpectedExceptionAttribute) and then verify that the DB is empty... to apply this behaviour use this pattern:
try
{
\\...
\\try to commit
}
catch(DbEntityValidationException ex)
{
\\do some validation, then:
throw;\\for ExpectedExceptionAttribute
}
I looked over the api of EntityTypeConfiguration, I didn't saw any contact which allows to UT the rules(unless you use tools like MsFakes, TypeMock Isolator there is no way to verify the ToTable/HasKey/Property was called). The class is being in used inside EntityFramework(which is a part of the BCL) in the integration test you don't have to verify that EntityFramework work properly, you are going to verify that your custom rules was integrated and works as you expect(In this answer you can read the reason not to test a BCL classes).
So use Moq in the UTs of AccountService. Create an integration tests for BlueMoonContext and RoleMap(without Moq).
By the way #LadislavMrnka offer an interesting way to test(integration test) EntityTypeConfiguration