I have this fluent configuration for my ApplicationSettings entity
builder.OwnsOne(
applicationSettings => applicationSettings.ActivitySettings,
builder =>
{
builder.Property(activitySettings => activitySettings.PastDaysAllowedThreshold)
.HasColumnName("ActivityPastDaysAllowedThreshold")
.HasDefaultValue(7);
builder.HasCheckConstraint(
"CK_ApplicationSettings_ActivityPastDaysAllowedThreshold",
"ActivityPastDaysAllowedThreshold >= 0");
});
Where ActivitySettings looks like this
public sealed class ActivitySettings // : ValueObject
{
public ActivitySettings(int pastDaysAllowedThreshold)
{
EnsureArg.IsGte(pastDaysAllowedThreshold, 0, nameof(pastDaysAllowedThreshold));
PastDaysAllowedThreshold = pastDaysAllowedThreshold;
}
public int PastDaysAllowedThreshold { get; } // already tried adding a setter.
}
And ApplicationSettings has this method
public void SetActivitySettings(ActivitySettings activitySettings)
{
EnsureArg.IsNotNull(activitySettings, nameof(activitySettings));
ActivitySettings = activitySettings;
RaiseDomainEvent(new ActivitySettingsUpdated(Id, activitySettings));
}
Everything is working perfectly, except for when I set PastDaysAllowedThreshold to 0. I can see that the tracked entity from the repo thinks that the value has been set to 0, but when I look in the database it has not changed.
What is going on
Solved this by updating the config to
builder.OwnsOne(
applicationSettings => applicationSettings.ActivitySettings,
builder =>
{
builder.Property(activitySettings => activitySettings.PastDaysAllowedThreshold)
.HasColumnName("ActivityPastDaysAllowedThreshold")
.HasDefaultValue(7)
.ValueGeneratedNever();
builder.HasCheckConstraint(
"CK_ApplicationSettings_ActivityPastDaysAllowedThreshold",
"ActivityPastDaysAllowedThreshold >= 0");
});
Notice the addition of ValueGeneratedNever()
Before adding this, for some reason ef was generating this in my snapshot
b1.Property<int>("PastDaysAllowedThreshold")
.ValueGeneratedOnAdd() // this was causing the issue
.HasColumnType("int")
.HasDefaultValue(7)
.HasColumnName("ActivityPastDaysAllowedThreshold");
And when profiling those queries when trying to set the value to 0, I could see that no SQL was being executed whatsoever.
Related
I already solved this problem, but I don't understand why this is happening. I would love a more detailed explanation.
For context, I have this class:
public class Test : Entity<Test>
{
public decimal DecimalProp { get; private set; }
}
And the EF mapping/config is like this:
public class TestConfig : IEntityTypeConfiguration<Test>
{
public void Configure(EntityTypeBuilder<Test> builder)
{
builder
.ToTable("Tests");
builder
.Property(t => t.DecimalProp)
.HasDefaultValue(-1)
.HasColumnType("decimal(4,1)")
.IsRequired();
}
}
When I add via EF a new Test with DecimalProp = 0, in SQL it saves as -1, but when I add with another number, like 5, it saves correctly.
I solved the problem using ValueGeneratedNever on the mapping class:
public class TestConfig : IEntityTypeConfiguration<Test>
{
public void Configure(EntityTypeBuilder<Test> builder)
{
builder
.ToTable("Tests");
builder
.Property(t => t.DecimalProp)
.HasColumnType("decimal(4,1)")
.ValueGeneratedNever()
.HasDefaultValue(-1)
.IsRequired();
}
}
My colleagues think that since 0 is the default value for C#'s decimal, EF thinks it should use the default value on SQL which is now -1, but for me it doesn't feel right, since 0 could be a valid value.
I've been searching for a solution to write a "generic" update method in EF Core which updates all changed properties of an entity including a related collection. The reason for this is that I store translations for the name of an entity in a different table.
I found this solution which seemed to work just fine at the beginning but then I noticed that I get an error "Database operation expected to affect 1 row(s) but actually affected 0 row(s)" when the only thing I changed in the entity was adding a new name translation in the related table TblProjectTranslations. Here is my code:
public async Task UpdateProjectAsync(TblProject updatedEntity)
{
TblProject dbEntity = Context.TblProjects
.Include(dbEntity => dbEntity.TblProjectTranslations)
.SingleOrDefault(dbEntity => dbEntity.ProjectId == updatedEntity.ProjectId);
if (dbEntity != null)
{
Context.Entry(dbEntity).CurrentValues.SetValues(updatedEntity);
foreach (TblProjectTranslation dbTranslation in dbEntity.TblProjectTranslations.ToList())
{
if (!updatedEntity.TblProjectTranslations
.Any(translation => translation.ProjectId == dbTranslation.ProjectId && translation.Language == dbTranslation.Language))
{
Context.TblProjectTranslations.Remove(dbTranslation);
}
}
foreach (TblProjectTranslation newTranslation in updatedEntity.TblProjectTranslations)
{
TblProjectTranslation dbTranslation = dbEntity.TblProjectTranslations
.SingleOrDefault(dbTranslation => dbTranslation.ProjectId == newTranslation.ProjectId && dbTranslation.Language == newTranslation.Language);
if (dbTranslation != null)
{
Context.Entry(dbTranslation).CurrentValues.SetValues(newTranslation);
}
else
{
dbEntity.TblProjectTranslations.Add(newTranslation);
}
}
await Context.SaveChangesAsync();
}
}
Here are the reverse engineered EF Core classes:
public partial class TblProject
{
public TblProject()
{
TblProjectTranslations = new HashSet<TblProjectTranslation>();
}
public int ProjectId { get; set; }
public virtual ICollection<TblProjectTranslation> TblProjectTranslations { get; set; }
}
public partial class TblProjectTranslation
{
public int ProjectId { get; set; }
public string Language { get; set; }
public string ProjectName { get; set; }
public virtual TblProject Project { get; set; }
}
Here is how they are defined in OnModelCreating:
modelBuilder.Entity<TblProject>(entity =>
{
entity.HasKey(e => e.ProjectId);
entity.ToTable("TBL_project");
entity.Property(e => e.ProjectId).HasColumnName("project_ID");
});
modelBuilder.Entity<TblProjectTranslation>(entity =>
{
entity.HasKey(e => new { e.ProjectId, e.Language });
entity.ToTable("TBL_project_translation");
entity.HasIndex(e => e.ProjectId, "IX_TBL_project_translation_project_ID");
entity.Property(e => e.ProjectId).HasColumnName("project_ID");
entity.Property(e => e.Language)
.HasMaxLength(5)
.HasColumnName("language")
.HasDefaultValueSql("('-')");
entity.Property(e => e.ProjectName)
.IsRequired()
.HasMaxLength(50)
.HasColumnName("project_name")
.HasDefaultValueSql("('-')");
entity.HasOne(d => d.Project)
.WithMany(p => p.TblProjectTranslations)
.HasForeignKey(d => d.ProjectId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("TBL_project_translation_TBL_project");
});
What I do not understand is that dbEntity.TblProjectTranslations.Add(newTranslation) seems to be the problem. When I replace this line with await Context.TblProjectTranslations.AddAsync(newTranslation), the error "magically" disappears, but aren't both ways supposed to do basically the same thing? Here is an example for an updatedEntity and a dbEntity which I captured while debugging the function right before the problem occurred:
updatedEntity:
ProjectId: 41
TblProjectTranslations: 1 Entry with:
Language: "en"
Project: null
ProjectId: 41
ProjectName: "TestNameEN"
dbEntity:
ProjectId: 41
TblProjectTranslations: No entries
What is going on here? I do not even have any triggers in the database in both of those tables which seemed to be the cause of this error for some other people.
After playing a bit with the sample model, code and explanations, I finally was able to reproduce it. The culprit seems to be the composite key and the default value for the string part of it:
entity.HasKey(e => new { e.ProjectId, e.Language }); // (1)
entity.Property(e => e.Language)
.HasMaxLength(5)
.HasColumnName("language")
.HasDefaultValueSql("('-')"); // (2)
Same happens if you use
.HasDefaultValue("-")
This combination somehow is causing the EF to consider the item added to the parent collection navigation property
dbEntity.TblProjectTranslations.Add(newTranslation);
as Modified instead of Added, which later leads to the error in questtion.
Since I can't find such behavior explanation in the EF Core documentation, and in general it looks wrong, I would suggest to go and report it to the EF Core GitHub issue tracker.
Meanwhile, the possible workarounds I see are
Remove .HasDefaultValueSql / .HasDefaultValue for the key column
Instead of adding the child to the parent collection, add it directly to the context or the corresponding DbSet (optionally set the reference navigation property in advance "just in case"):
newTranslation.Project = dbEntity;
Context.Add(newTranslation);
I was wondering if there is any way to set a value to an entity onsave?
Because I'm working on a multi tenant web application and I would like to set the the current tenant ID (through simple DI service).
I tried using HasDefaultValue() in Fluent API, however this will try to convert to a SQL function. So this doesn't work for me.
builder.Entity<Order>( )
.HasQueryFilter(p => p.TenantId == _tenantProvider.GetTenantId())
.Property(p => p.TenantId)
.HasDefaultValue(_tenantProvider.GetTenantId());
Any suggestions are greatly appreciated.
You could override the DbContext.SaveChanges() method and iterate the ChangeTracker entries:
public override int SaveChanges()
{
foreach (var entityEntry in ChangeTracker.Entries()) // Iterate all made changes
{
if (entityEntry.Entity is Order order)
{
if (entityEntry.State == EntityState.Added) // If you want to update TenantId when Order is added
{
order.TenantId = _tenantProvider.GetTenantId();
}
else if (entityEntry.State == EntityState.Modified) // If you want to update TenantId when Order is modified
{
order.TenantId = _tenantProvider.GetTenantId();
}
}
}
return base.SaveChanges();
}
Of course, this needs the tenant provider to be injected into your context.
EF Core value generation on add with custom ValueGenerator
Generates values for properties when an entity is added to a context.
could be utilized to assign TenantId to the new entities. Inside the Next method you could obtain the TenantId from the context (or some service).
Taking your sample, the value generator could be a nested class inside your DbContext like this:
class TenantIdValueGenerator : ValueGenerator<int>
{
public override bool GeneratesTemporaryValues => false;
public override int Next(EntityEntry entry) => GetTenantId(entry.Context);
int GetTenantId(DbContext context) => ((YourDbContext)context)._tenantProvider.GetTenantId();
}
The all you need is to assign the generator to TenantId property using some of the HasValueGenerator fluent API.
The only problem is that by design the value generators are called only if the property does not have explicitly set value (for int property - if the value is 0).
So the better approach it to abstract (and fully control) the TenantId property by removing it from entity models and replacing it with shadow property.
Hence my suggestion is, remove the TenantId from entity classes and call the following method inside your OnModelCreating for each entity that needs TenantId column:
void ConfigureTenant<TEntity>(ModelBuilder modelBuilder) where TEntity : class
{
modelBuilder.Entity<TEntity>(builder =>
{
builder.Property<int>("TenantId")
.HasValueGenerator<TenantIdValueGenerator>();
builder.HasQueryFilter(e => EF.Property<int>(e, "TenantId") == _tenantProvider.GetTenantId());
});
}
If you are using EF Core 5+, then you have the option of using the SavingChanges event. This will allow you to set your custom logic for both SaveChanges and SaveChangesAsync without having to override both methods.
Example:
public MyDbContext(DbContextOptions<MyDbContext> dbContextOptions, ITenantProvider tenantProvider) : base(dbContextOptions)
{
SavingChanges += (sender, args) =>
{
foreach (var orderEntity in ChangeTracker.Entries<Order>())
{
if (orderEntity.State == EntityState.Added)
{
orderEntity.Entity.TenantId = tenantProvider.GetTenantId();
}
}
};
}
I have ASP.NET MVC 5 application that uses Entity Framework 6.2. This app creates some records in MS SQL server.
I have to process this records in AWS lambda function. For this purpose, I created c# .NET Core v2.0 lambda function. There I generate models from existed DB as described in the next article - Getting Started with EF Core on ASP.NET Core with an Existing Database.
Generated models look like:
public partial class Request
{
public int EntityId { get; set; }
public Result Result { get; set; }
}
public partial class Result
{
public int EntityId { get; set; }
public Request Entity { get; set; }
}
And OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Request>(entity =>
{
entity.HasKey(e => e.EntityId);
entity.ToTable("Request", "mySchema");
});
modelBuilder.Entity<Result>(entity =>
{
entity.HasKey(e => e.EntityId);
entity.ToTable("Result", "mySchema");
entity.HasIndex(e => e.EntityId).HasName("IX_EntityId");
entity.Property(e => e.EntityId).ValueGeneratedOnAdd();
entity.HasOne(d => d.Entity)
.WithOne(p => p.Result)
.HasForeignKey<Result>(d => d.EntityId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_mySchema.Result_mySchema.Request_EntityId");
});
}
In the lambda function, I am processing a request and setting result:
using (var db = new DataContext(connection))
{
var request = db.Request.First();
request.Result = new Result();
db.SaveChanges();
}
On the save operation I got the exception:
Cannot insert explicit value for identity column in table 'Result' when IDENTITY_INSERT is set to OFF.
I checked different way to solve this issue. But there are already set ValueGeneratedOnAdd function in model creating method.
Also, I tried to add [DatabaseGenerated(DatabaseGeneratedOption.Identity)] to Result.EntityId - without any success.
What did I miss in my implementation?
You are trying to initialize request.Result with new Result() which has no values. That may cause this error.
I'm currently using the simple model below. It's pretty straightforward: we have resources and they can be Room, EmptyOffice (...) or Service.
Room and EmptyOffice can have a capacity, but NOT Service.
public abstract class Resource : Entity
{
public string Name { get; set; }
}
public class Room : Resource
{
public int Capacity { get; set; }
}
public class EmptyOffice : Resource
{
public int Capacity { get; set; }
}
public class Service : Resource
{ }
To get the data from my SQL view, I use the mappings:
builder.Entity<Resource>(m =>
{
m.ToTable("resource", "facility");
m.HasKey(x => x.Id);
m.Property(x => x.Id)
.HasColumnName("ResourceId");
m.Property(x => x.Type)
.HasColumnName("ResourceTypeId");
m.HasDiscriminator(x => x.Type)
.HasValue<Room>(ResourceType.Room)
.HasValue<EmptyOffice>(ResourceType.EmptyOffice)
.HasValue<Service>(ResourceType.Service);
});
builder.Entity<Room>();
builder.Entity<EmptyOffice>();
builder.Entity<Service>();
When I run my code, EF Core throws the following exception:
System.Data.SqlClient.SqlException: 'Invalid column name 'Room_Capacity'.'
If I rename the Capacity property to Room_Capacity, it works but it's horrible.
How can I force EF Core 2.0 to target the capacity property for each of my child entities?
Thank you
Sebastien
This worked for me:
builder.Entity<Room>().Property(a => a.Capacity).HasColumnName("Capacity");
builder.Entity<EmptyRoom>().Property(a => a.Capacity).HasColumnName("Capacity");
You can't do that as the only inheritance pattern available in EF Core is table per class hierarchy. If you go with interfaces instead of base classes, you can, but each entity will be mapped to a different table. Mark any property you want to exclude with [NotMapped], or, using the code, with Ignore.
I did in project next code to make it more generic way.
private static void FindAndConfigureBackgroundJobResultTypes(ModelBuilder modelBuilder)
{
var backgroundJobResultTypes = typeof(BackgroundJobResult).Assembly.GetTypes().Where(x => x.IsSubclassOf(typeof(BackgroundJobResult))).ToList();
var sameTypeAndNameProperties = backgroundJobResultTypes
.SelectMany(x => x.GetProperties())
.GroupBy(d => new {d.Name, d.PropertyType})
.Select(grp => new
{
PropertyType = grp.Key.PropertyType,
PropertyName = grp.Key.Name,
Count = grp.Count()
})
.Where(x => x.Count > 1).ToList();
foreach (var backgroundJobResultType in backgroundJobResultTypes)
{
//Set base type , instead of exposing this type by DbSet
modelBuilder.Entity(backgroundJobResultType).HasBaseType(typeof(BackgroundJobResult));
//Map properties with the same name and type into one column, EF Core by default will create separate column for each type, and make it really strange way.
foreach (var propertyInfo in backgroundJobResultType.GetProperties())
{
if (sameTypeAndNameProperties.Any(x => x.PropertyType == propertyInfo.PropertyType && x.PropertyName == propertyInfo.Name))
{
modelBuilder.Entity(backgroundJobResultType).Property(propertyInfo.PropertyType, propertyInfo.Name).HasColumnName(propertyInfo.Name);
}
}
}
}