I have a SQL Server table with certain fields that are set by the database via default values that, once saved, should never been modified again (e.g. DateCreated).
In the Entity Framework Core 2.1 model builder or classes, how do we "mark" a field as essentially read-only? In other words, I don't want any code to be able to set or overwrite these fields.
Based on my searching, would I add .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity) at the end of .Property()?
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Doohicky>(entity =>
{
... // other fields
entity.Property(e => e.DateCreated).HasDefaultValueSql("(getdate())");
... // other fields
});
}
Or do I add a [DatabaseGenerated(DatabaseGeneratedOption.Identity)] annotation to the DateCreated field?
public class Doohicky
{
public DateTime DateCreated {get; set;}
}
Or is there another way entirely?
I want it such that in the future, if anybody decides to write something like this, an error would be thrown.
model.DateCreated = new DateTime();
dbContext.SaveChanges() // errors out
Any insight would be greatly appreciated.
The EF Core intended way is to set AfterSaveBehavior property to value other than the default Save:
Gets a value indicating whether or not this property can be modified after the entity is saved to the database.
If Throw, then an exception will be thrown if a new value is assigned to this property after the entity exists in the database.
If Ignore, then any modification to the property value of an entity that already exists in the database will be ignored.
There is no dedicated fluent API yet, so you need to set it directly through mutable property metadata like this:
entity.Property(e => e.DateCreated)
.HasDefaultValueSql("(getdate())")
.Metadata.AfterSaveBehavior = PropertySaveBehavior.Throw; // <--
Update (EF Core 3.x): Starting with EF Core 3.0, many properties like this have been replaced with Get / Set extension method pairs, so the relevant code now is as follows:
.Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Throw);
[Required, DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime DateCreated {get; set;}
I've done this in the past with auditable properties such as DateCreated, DateModified, etc. This solution probably isn't ideal for excluding specific properties in various objects (although you could probably do something with a custom attribute, etc.).
I override SaveChanges/Async(), then loop through all the changed objects that the context is tracking. All of my objects use the same base class so I can achieve this through the following:
var changes = ChangeTracker.Entries<BaseEntity>().Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);
With those objects, I loop over them and set some auditable properties, or ignore certain properties if the object isn't new. First, I have a collection of strings which represent property names that I want to exclude. I then loop over the collection and ignore the properties where the property name matches that of the excluded collection. See below:
// A collection of property names which should not be updated
var excludedProperties = new[] { "CreatedBy", "CreatedDateUtc" };
foreach (var change in changes)
{
// If new, do as you'd like
// If used, ignore date created
Array.ForEach(excludedProperties, prop =>
{
change.Property(prop).IsModified = false;
});
}
Related
I have a one-to-many relationship with an order and some orderlines.
When updating the runtime from 2.0.? to NetCore 2.2.7 I have started to get this message:
System.InvalidOperationException: The property 'OrderId' on entity
type 'OrderLineDto' has a temporary value. Either set a permanent
value explicitly or ensure that the database is configured to generate
values for this property.
Did anything change when updating, and what exactly is this error? In the OrderlineDto I do have an OrderId, but I have never done anything to maintain it, nor would I assume I should.
Simplified my code looks like this:
Order o = new OrderDto() {Foo = "Foo"};
o.Lines.Add(new OrderlineDto(whatever) {Bar = "Bar"});
_db.Orders.Add(o);
_db.SaveChanges();
OrderlineDto is database first and looks like this:
[Key]
[Column("Id_pk")]
public int Pk { get; set; }
public int OrderId { get; set; }
public virtual OrderDto Order { get; set; }
This also seems to still work on some computers, but not on others. So it could be a bad installation or something, but if a codechange could be made that solves it explicit then I am also interested in that.
it seems like the entity framework doesn't recognize the one-to-many relationship
you can try to add this code on your OnModelCreating()
modelBuilder.Entity<OrderlineDto>()
.HasOne(ol => ol.OrderDto)
.WithMany()
.HasForeignKey(p => p.OrderId);
then add migration and check if there is any updates on database
For some reason the problem was a wrong ChangeTracker. My solution was to manually detatch everything on every save. I'll leave the solution here if others search for something similar.
var changedEntriesCopy = _db.ChangeTracker.Entries().ToList();
await _db.SaveChangesAsync();
foreach (var entry in changedEntriesCopy)
entry.State = EntityState.Detached;
In my app I am trying to follow DDD whilst modelling a simple fund account.
The classes are
FundAccount
public Guid Id { get; set; }
private ICollection<AccountTransaction> _transactions = new List<AccountTransaction>();
public IReadOnlyCollection<AccountTransaction> Transactions => _transactions
.OrderByDescending(transaction => transaction.TransactionDateTime).ToList();
AccountTransaction
public Guid Id { get; set; }
public decimal Amount { get; set; )
I am trying to retrieve the fund account from the database with the transactions included with the following:
var fundAccount = await _context.FundAccounts
.Include(a => a.Transactions)
.SingleAsync(a => a.Id == ...);
When I retrieve the FundAccount (which has transactions in the database) Transactions has 0 AccountTransaction?
Can anyone see what I need to do here?
First, when using *domain logic" in the entity data model (which you shouldn't, but that's another story), make sure to configure EF Core to use backing fields instead of properties (the default), by adding the following to the db context OnModelCreating override:
modelBuilder.UsePropertyAccessMode(PropertyAccessMode.Field);
This btw has been recognized as issue and will be fixed in the 3.0 version - see Breaking Changes - Backing fields are used by default. But currently you have to include the above code.
Second, you have to change the backing field type to be compatible with the property type.
In your case, ICollection<AccountTransaction> is not compatible with IReadOnlyCollection<AccountTransaction>, because the former does not inherit the later for historical reasons. But the List<T> (and other collection classes) implement both interfaces, and since this is what you use to initialize the field, simply use it as a field type:
private List<AccountTransaction> _transactions = new List<AccountTransaction>();
With these two modifications in place, the collection navigation property will be correctly loaded by EF Core.
Assume I have an EF entity class Person, with a PhoneNumber on it. PhoneNumber is stored as a string type, but I want all accesses on the Person to go through Phone which has some nice accessor functions, e.g. validation or GetAreaCode(). I want to back it in the db as a string, but when queried for it I want to return it as a PhoneNumber:
public class Person {
public PhoneNumber Phone { /* Some clever get/set logic here */ }
private string _phoneNumber; // Backing field
}
Or can I get PhoneNumber to store itself as a string? If I simply include it in the model by removing the backing field above, EF gets confused by the constructors (a protected ctor with some more args than the one string) and also a copy ctor PhoneNumber(PhoneNumber other). Can i make EF ignore those somehow?
I'm open to ideas...
You can use #nbrosz's answer to fix your issue but you no longer need to do this kind of workaround if you're using EF Core 2.1. You can rid of the backing field by using EF Core 2.1 (which is in Release Candidate 1 since 7 May 2018) you can use the feature of Value Conversion explained here by Microsoft:
Value converters allow property values to be converted when reading
from or writing to the database. This conversion can be from one value
to another of the same type (for example, encrypting strings) or from
a value of one type to a value of another type (for example,
converting enum values to and from strings in the database.)
So for your case, you can just remove the backing field. You no longer need it. Your class should look like this:
public class Person
{
public PhoneNumber Phone { /* Some clever get/set logic here */ }
}
And in your OnModelCreating method, you configure the conversion like below:
modelBuilder.Entity<Person>()
.Property(p => p.Phone)
.HasConversion(
phone => {
// Here you code the logic of how to get the value to store in DB
return ...;
},
dbValue => {
// Here you code the logic of how to construct the PhoneNumber instance from the value to store in DB
}
);
That's it. Actually it is in release candidate but Microsoft says:
EF Core 2.1 RC1 is a “go live” release, which means once you test that
your application works correctly with RC1, you can use it in
production and obtain support from Microsoft, but you should still
update to the final stable release once it’s available.
This remainder of my answer is for #nbrosz because you are dealing with enum type. You can remove the backing field and also you can one of the many built-in value converters provided by EF Core 2.1. For enum to string value conversion we have the type EnumToStringConverter. For the logic you're doing in your answer, you can just simplify it to this for entity:
[Display(Name = "Fire Type")]
public Enums.FireType Type { get; set; }
We removed the NotMapped attribute on the property and there is no logic y for conversion.
In your OnModelCreating method you do this:
var converter = new EnumToStringConverter<FireType>();
modelBuilder
.Entity<Fire>()
.Property(e => e.FireType)
.HasConversion(converter);
You can also let EF Core detects the right converter for you by using the generic version of HasConversion<T> like below:
modelBuilder
.Entity<Fire>()
.Property(e => e.FireType)
.HasConversion<string>();
If you don't like to use fluent configuration you can use Column data annotation attibute like below and EF Core will do the conversion for you:
[Column(TypeName = "nvarchar(20)")]
[Display(Name = "Fire Type")]
public Enums.FireType Type { get; set; }
The only way I've found that works in EF Core 2.0 is to create a public property with getters/setters with a name that does not match your backing field and mark it as NotMapped, like so:
[NotMapped]
[Display(Name = "Fire Type")]
public Enums.FireType Type
{
get
{
Enums.FireType type;
if (!Enum.TryParse(_fireType, out type))
type = Enums.FireType.Fire; // default
return type;
}
set
{
_fireType = value.ToString();
}
}
private string _fireType;
Then in your DbContext's OnModelCreating method, tell it to create a column on the database table that acts like a backing property:
// backing properties
modelBuilder.Entity<Fire>()
.Property<string>("FireType")
.HasField("_fireType")
.UsePropertyAccessMode(PropertyAccessMode.Field);
With that, I was finally able to create a successful migration that allowed me to have a private field on my model, a public transformative property on the model, and a single properly-named column in my database table. The only catch is that the public property and the private field can't share the same name (without sharing the same type), but that wasn't going to be the case for you, anyway.
I know there are several questions posed about this very same thing but none of which seems to help me. I'm trying to do a .RemoveRange() and every question I've been seeing has to do with edits and adds.
Here's the relevant bits of the method in which the exception is getting thrown:
public bool UpdateFileboundApplications(IList<IFileboundApplicationDm> fileboundApplications)
{
// get all mappings in the DB that match the incoming fileboundApplications
var incomingFbAppsAlreadyExistingInDb =
fileboundApplications.Where(app => app.Id == Db.inf_DMS_FBApplicationProjectMapping.SingleOrDefault(a => a.ApplicationId == app.Id)?.ApplicationId
&& app.FileboundProject != null).ToList();
// in the case that application/project mappings include filebound applications with no project mapping,
// pass the collection to a method which will handle removal of these records.
var fbAppMappingsWithoutNulls = RemoveNullFileboundApplicationMappings(incomingFbAppsAlreadyExistingInDb, fileboundApplications);
var fbAppMappingsAppIdsAndProjectIds = fbAppMappingsWithoutNulls.Select(x => new { appId = x.Id, projectId = x.FileboundProject.Id}).ToList();
var dbRecords = Db.inf_DMS_FBApplicationProjectMapping.Select(y => new { appId = y.ApplicationId, projectId = y.ProjectID}).ToList();
var fbApplicationDifferences =
dbRecords.FindDifferences(fbAppMappingsAppIdsAndProjectIds,
s => new Tuple<int, int>(s.appId, s.projectId),
d => new Tuple<int, int>(d.appId, d.projectId));
if (fbApplicationDifferences.ExistOnlyInSource.Any())
{
// items to remove from the table, as these apps are now assigned to a different project.
var allAppsToRemove = fbApplicationDifferences.ExistOnlyInSource.Select(x => new inf_DMS_FBApplicationProjectMapping
{
ApplicationId = x.appId,
ProjectID = x.projectId,
MapId = Db.inf_DMS_FBApplicationProjectMapping.Single(m => m.ApplicationId == x.appId).MapId
}).ToList();
Db.inf_DMS_FBApplicationProjectMapping.RemoveRange(allAppsToRemove);
}
Db.SaveChanges();
return true;
}
FWIW, I'll include the code for the RemoveNullFileboundApplicationMappings as well:
private IEnumerable<IFileboundApplicationDm> RemoveNullFileboundApplicationMappings(IEnumerable<IFileboundApplicationDm> incomingFbAppsAlreadyExistingInDb,
IEnumerable<IFileboundApplicationDm> fileboundApplications)
{
// hold a collection of incoming fileboundApplication IDs for apps that have no associated fileboundProject
var appIdsWithNoFbProject = fileboundApplications.Except(incomingFbAppsAlreadyExistingInDb)
.Select(app => app.Id);
// get records in the table that now need to be removed
var dbRecordsWithMatchingIds = Db.inf_DMS_FBApplicationProjectMapping.Where(mapping => appIdsWithNoFbProject.Contains(mapping.ApplicationId));
if (dbRecordsWithMatchingIds.Any())
{
// remove records for apps that no will no longer have an associated Filebound project
Db.inf_DMS_FBApplicationProjectMapping.RemoveRange(dbRecordsWithMatchingIds);
Db.SaveChanges();
}
return fileboundApplications.Where(app => app.FileboundProject != null);
}
Finally, here's the inf_DMS_FBApplicationProjectMapping class:
public partial class inf_DMS_FBApplicationProjectMapping
{
public int MapId { get; set; } // <-- this is the PK
public int ApplicationId { get; set; }
public int ProjectID { get; set; }
public Nullable<int> Modified_By { get; set; }
public Nullable<System.DateTime> Modified_On { get; set; }
public virtual glb_Applications glb_Applications { get; set; }
}
}
Exception reads as follows:
{"Attaching an entity of type 'xxxx' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values.
In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate."}
I don't quite understand how I need to be using Db.inf_.....Add(), as I'm not intending to add records to the table; I need to be removing records.
I don't understand what this "attaching to context" is all about and what that really means.
I really appreciate any insight the community may have on this. It's been a struggle trying to find a way to solve this. Thanks!
I guess the problem is in the new that you use to compose the list you pass as parameter to RemoveRange. As the entities in that list have not been queried directly from your DbSet they have never been attached to your local context and so EF gets confused.
You need to understand the concept of entities attached to the context. Entity Framework keeps track of the changes done to entities you are working with, in order to be able to decide what to do when you do SaveChanges: insert, update, delete. EF is only able to do that if the entities are attached to the context. That means they have a property State with the value Added, Deleted, Modified, Unchanged, etc.
In simple scenarios this is transparent to you, because entities get automatically attached when you do DbSet.Add(entity), DbSet.Find(entityId), or when you get an entity instance as a result of a query, like DbSet.Where(...), DbSet.FirstOrDefault(...), etc. That is why you probably never had to worry about attached entities before in your EF code.
In more complex scenarios like your current one, the entities you are trying to delete have not been instantiated from one of those operations, so they have not been automatically attached to your context. You have to do it explicitly, if you instantiate them with new.
So you should do something like this before the SaveChanges:
foreach(var item in allAppsToRemove)
{
Db.Entry(item).State = EntityState.Deleted;
}
By using the method Entry the entities get attached to the context, and then you explicity set their state as Deleted, to have them deleted when SaveChanges is executed later.
Take a look at this page. Even if it deals mostly with Add and Update cases it contains information relevant to your problem with the Delete. Understanding the concept of entities attached to the local DbContext will help you a lot when programming with EF. There are some cases like this one where you will have trouble if you don't know how attached entities work (you will eventually get to some 'orphaned children' errors also).
Note: in Entity Framework Core (EF7) there is an AttachRange method that can be used before RemoveRange.
With Diana's help, I was able to solve this issue.
The problem was that I was manually flipping the entity state AND calling .RemoveRange(). I only needed to be flipping the entity state. Here's the relevant bits that solved the issue:
...
...
...
if (fbApplicationDifferences.ExistOnlyInSource.Any())
{
// items to remove from the table, as these apps are now assigned to a different project.
var allAppsToRemove = fbApplicationDifferences.ExistOnlyInSource.Select(x => new inf_DMS_FBApplicationProjectMapping
{
ApplicationId = x.appId,
ProjectID = x.projectId,
MapId = Db.inf_DMS_FBApplicationProjectMapping.Single(m => m.ApplicationId == x.appId).MapId
}).ToList();
foreach (var app in allAppsToRemove)
{
var item = Db.inf_DMS_FBApplicationProjectMapping.Find(app.MapId);
Db.Entry(item).State = EntityState.Deleted;
}
//Db.inf_DMS_FBApplicationProjectMapping.RemoveRange(allAppsToRemove); <-- these items are already "flagged for deletion" with .State property change a few lines above.
}
Just change your code after SaveChanges methot change EntityState Detached
I am trying to simply load an entity, modify a property and then save it back to the database.
var db = new NewsletterContext();
var newsletter = db.Newsletters.Find(x => x.ID==newsletterID);
newsletter.SomeProperty = 5;
db.SaveChanges();
This causes a validation error as there are some properties on the newsletter object which are required and apparently not loaded when I do a Find().
I can solve this using an Include() for each required property followed by a Where():
var db = new NewsletterContext();
var newsletter = db.Newsletters.Include(x => x.RequiredProp1)
.Include(x => x.RequiredProp2).Include(x => x.RequiredProp3)
.Where(x => x.ID==newsletterID)
.FirstOrDefault();
db.SaveChanges();
This isn't a very elegant solution and will break if I add more required properties to the Newsletter object.
Is there a better solution?
Entity framework will disable lazy loading when doing the validation. Hence if you put required validation on navigational properties the validation will fail. You can decorate the scalar property related to the navigational property instead.
public class Foo
{
[Required]
public int? RequiredScalarId { get; set; }
public virtual Bar RequiredNavigationalProp { get; set; }
}
Do not want to pollute your model with foriegn keys? Good! Read on.
Instead you can override the SaveChanges method in NewsletterContext.
Get all modified entities and using reflection, load all their lazy loaded properties and collections using the following sample syntax:
db.Entry(newsletter).Reference(n => n.RequiredProp1).Load();
db.Entry(newsletter).Collection(n => n.RequiredCollec1).Load();
It won't be easy as it sounds, but once you are have written it, it would feel so good to see it work seamlessly :)