I am having troubles trying to figure out how to use the EF6 interceptors to set a value on Insert/Update.
What I wanted to do is to have an interceptor to automatically create a new instance of Audit like so:
public class FooContext : DbContext
{
public DbSet<Invoice> Invoices { get; set; }
public DbSet<Audit> Audits { get; set; }
}
public class Invoice
{
public int Id { get; set; }
public string Name { get; set; }
public Audit AuditAndConcurrencyKey { get; set; }
}
public class InvoiceItem
{
public int Id { get; set; }
public Invoice Header { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
//For legacy reasons. I know this design is wrong :(
public Audit AuditAndConcurrencyKey { get; set; }
}
public class Audit
{
public int Id { get; set; }
public int InstanceId { get; set; }
public string Message { get; set; }
}
[Test]
public void WillCreateAudit()
{
using (var db = new FooContext())
{
var inv = new Invoice {Name = "Foo Invoice"};
var invLine = new InvoiceItem {Header = inv, Price = 1, Name = "Apple"};
db.Invoices.Add(inv);
db.SaveChanges();
//Inceptors should figure out that we are working with "Invoice" and "InvoiceLine"
//And automatically create an "Audit" instance
Assert.That(inv.AuditAndConcurrencyKey != null);
Assert.That(invLine.AuditAndConcurrencyKey != null);
Assert.That(inv.AuditAndConcurrencyKey == invLine.AuditAndConcurrencyKey)
}
}
The first thing I checked is this example for SoftDeleteInterceptor. I don't think this is what I want because it looks like at the point where we are already generating the expression tree, we are no longer aware of the type of object you are working with.
I checked this example as well, but again, it looks like we are injecting strings instead of setting object references.
Ideally I want something like this:
public class AuditInterceptor
{
public void Intercept(object obj)
{
if (!(obj is Invoice) && !(obj is InvoiceItem))
return; //not type we are looking for, by-pass
//Set the audit here
}
}
Related
When trying to create a new database entry of type TestForm2 I include the related object Unit Type's ID as a foreign key, except when I perform context.SaveChanges() after adding the new model I get the following SQL exception:
SqlException: Violation of PRIMARY KEY constraint 'PK_dbo.UnitTypes'. Cannot insert duplicate key in object 'dbo.UnitTypes'. The duplicate key value is (2d911331-6083-4bba-a3ad-e50341a7b128). The statement has been terminated.
What this means to me is that it thinks that the foreign entry I'm trying to relate to the new model is instead a new object that it's attempting to insert into the UnitTypes table and failing because it sees an existing entry with the same primary key.
For context (pun not intended), this is my data context, the database model, and the erroring "Create" function.
public class DataContext : IdentityDbContext<ApplicationUser>
{
public DataContext() : base("DefaultConnection")
{
}
public static DataContext Create()
{
return new DataContext();
}
public DbSet<SafetyIncident> SafetyIncidents { get; set; }
public DbSet<ProductionLine> ProductionLines { get; set; }
public DbSet<ProductionOrder> ProductionOrders { get; set; }
public DbSet<SerialOrder> SerialOrder { get; set; }
public DbSet<QualityError> QualityErrors { get; set; }
public DbSet<PSA> PSAs { get; set; }
public DbSet<TestStation> TestStations { get; set; }
public DbSet<ProductionGoal> ProductionGoals { get; set; }
public DbSet<DailyWorkStationCheck> DailyWorkStationChecks { get; set; }
public DbSet<TestForm> TestForms { get; set; }
public DbSet<User> AppUsers { get; set; }
public DbSet<Options> Options { get; set; }
public DbSet<DriveList> DriveSerials { get; set; }
public DbSet<MRPController> MRPControllers { get; set; }
public DbSet<TestOption> TestOptions { get; set; }
public DbSet<UnitType> UnitTypes { get; set; }
public DbSet<UnitTypeMap> UnitTypeMaps { get; set; }
public DbSet<TestForm2> TestForm2s { get; set; }
public DbSet<TestFormSection> TestFormSections { get; set; }
public DbSet<TestFormSectionStep> TestFormSectionSteps { get; set; }
}
public class TestForm2 : BaseEntity
{
public string SerialNumber { get; set; }
public string MaterialNumber { get; set; }
public string UnitTypeId { get; set; }
public UnitType UnitType { get; set; }
public bool UsesStandardOptions { get; set; }
public bool OptionsVerified { get; set; } // This will only be used when UsesStandardOptions is true, otherwise its value doesn't matter
public ICollection<TestOption> AllOptions { get; set; } // List of all options (at time of form creation)
public ICollection<TestOption> Options { get; set; } // The options on a unit
public ICollection<TestFormSection> Sections { get; set; }
}
public FormViewModel Create(FormViewModel vm)
{
using (var context = new DataContext())
{
List<string> optionListStrings = GetOptionListForModelNumber(vm.MaterialNumber); // returns list of option codes
List<TestOption> matchingOptions = context.TestOptions
.Where(optionInDb =>
optionListStrings.Any(trimOption => trimOption == optionInDb.OptionCode)).ToList();
var unitType = context.UnitTypes.FirstOrDefault(x => x.Name == vm.UnitType);
string unitTypeId = unitType.Id;
TestForm2 newForm = new TestForm2
{
// ID & CreatedAt instantiated by Base Entity constructor
SerialNumber = vm.SerialNumber,
MaterialNumber = vm.MaterialNumber,
UnitTypeId = unitType.Id,
UsesStandardOptions = vm.UsesStandardOptions,
OptionsVerified = vm.OptionsVerified,
//AllOptions = context.TestOptions.ToList(),
//Options = matchingOptions,
Sections = vm.Sections,
};
context.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
context.TestForm2s.Add(newForm);
context.SaveChanges(); // THIS IS WHERE THE SQL EXCEPTION IS HAPPENING
return vm;
}
return null;
}
Lastly, I'm not sure if it's relevant, but a full copy of the related UnitType is viewable as part of newForm only after context.TestForm2s.add(newForm) resolves. This is weird to me since I don't think it should be automatically relating the data object like that.
I haven't been able to try much since everything looks properly configured to me. Please let me know if this is not the case or if I should include any other info.
Found the issue. The vm.Sections was not using viewmodels to contain the section data, so the vm.Sections contained UnitType database models. Since this was instantiated in the controller (before opening the data context in the TestForm2 Create method) EF assumed that these data were new and needed to be added to the UnitType table.
Hope this thread helps someone else running into similar issues.
I have a web service that users can send for that information like the following example.
The problem occurs when the information sent in a class is the same. Like a "sampleLine" in code.
How can I control it not to be stored in the duplicate information "sampleLine" table?
public class samplePerson
{
public int ID { get; set; }
public string Name { get; set; }
public sampleCopmany PersonCopmany { get; set; }
public sampleLine PersonLine { get; set; }
}
public class sampleCopmany
{
public int ID { get; set; }
public string Name { get; set; }
public sampleLine CopmanyLine { get; set; }
}
public class sampleLine
{
public int ID { get; set; }
public string Name { get; set; }
}
public class sampleDBContext
{
private MyDBContext dBContext;
public sampleDBContext()
{
dBContext = new MyDBContext();
}
public void Save()
{
samplePerson samplePerson = new samplePerson();
samplePerson.ID = -1;
samplePerson.Name = "Reza";
samplePerson.PersonCopmany = new sampleCopmany()
{
ID = -1,
Name = "Test",
CopmanyLine = new sampleLine()
{
ID = -1,
Name = "line"
}
};
samplePerson.PersonLine = new sampleLine()
{
ID = -1,
Name = "line"
};
dBContext.Add(samplePerson);
dBContext.SaveChangesAsync();
}
}
Is it possible to control this item?
There is not any automatic way in EF that handles it for you and you need to manually check for the existence of data before creating it.
If the record exists, then use the existing key and if not then create the record as you have done in your sample code.
In frontend in Angular I created the possibility of adding tags and memes. In the back-end in the web api I would like to save the tags in the database. Using entity framework code first, I created the structure of three tables:
public class Memes
{
public int Id { get; set; }
public string MemName { get; set; }
public string UserId { get; set; }
public string Image { get; set; }
public List<Memes_tags> MemesIteam { get; set; }
}
public class HashTag
{
public int Id { get; set; }
public int MemesId { get; set; }
public string Name { get; set; }
}
public class Memes_tags
{
public int Id { get; set; }
public int MemesId { get; set; }
public int HashTagId { get; set; }
public virtual Memes Memes { get; set; }
public virtual HashTag HashTags { get; set; }
}
Then I created a method that should save memes and tags in the database:
[HttpPost]
[Route("api/Memes/AddMemes")]
public IHttpActionResult CreateMemes([FromBody] MemHashTagsViewModel createMem)
{
ApplicationDbContext db = new ApplicationDbContext();
if (createMem != null)
{
Memes mem = new Memes()
{
MemName = createMem.MemName,
Image = createMem.Image,
UserId = createMem.UserId
};
db.MemesModel.Add(mem);
foreach (var item in createMem.HashTags)
{
var hashTag = new HashTag()
{
MemesId = mem.Id,
Name = item
};
db.HashTags.Add(hashTag);
}
db.SaveChanges();
return Ok();
}
else
{
return NotFound();
}
}
Incoming data:
I have problem with the correct Memes Id record. For example, I created a mem that has Id = 4 and in the table HashTags should be 4 and in my case is 0.
Is there any other better solution for saving tags in the database? Is my solution is good?
Yeah, the thing is: since you didn't save the mem first, it doesn't have an ID when you add it to the hashtag.
If you want to do it that way, you should make HashTag a member, in form of a list (property) on the mem. Then, when creating the HashTag objects, not add a member ID. The merely add the Mem to the database, and EF will take care of the object structure.
(On my phone, will make a code example in the morning if no one beats me to it)
EDIT: Here's how i would do it:
Respectfully: Drop the Memes_tags class as their seems to be no point in having it at all. It merely works as a relation between Memes and HashTags, but that already exists.
For purposes of Best practice, at least according to MS's own EF 'get start' doc the id of the class should be named: <class_name>Id, so that has been 'corrected' as well.
public class Memes
{
public int MemesId { get; set; }
public string MemName { get; set; }
public string UserId { get; set; }
public string Image { get; set; }
public List<HashTag> HashTags { get; set; }
}
public class HashTag
{
public int HashTagId { get; set; }
public int MemesId { get; set; }
public string Name { get; set; }
public virtual Memes { get; set; }
}
Below is the modified 'CreateMemes'. The idea is, that instead of adding the ID of the 'Memes' to hashtag, we merely add the HashTags to the meme object, thus they are add to EF as well, and when the 'Memes' record is add to the database, EF will make certain to create the hashtags too.
[HttpPost]
[Route("api/Memes/AddMemes")]
public IHttpActionResult CreateMemes([FromBody] MemHashTagsViewModel createMem)
{
ApplicationDbContext db = new ApplicationDbContext();
if (createMem != null)
{
Memes mem = new Memes()
{
MemName = createMem.MemName,
Image = createMem.Image,
UserId = createMem.UserId
};
foreach (var item in createMem.HashTags)
{
var hashTag = new HashTag()
{
Name = item
};
mem.HashTags.add(hashTag);
}
db.add(mem);
db.SaveChanges();
return Ok();
}
else
{
return NotFound();
}
}
Just adding the created instance to the context entity model isn't enough db.MemesModel.Add(mem);. Id value doesn't gets generated unless you call SaveChanges() on it. This in your below code there is no Id value yet and so what you observe
var hashTag = new HashTag()
{
MemesId = mem.Id,
Name = item
};
I am using Entity Framework code first with fluent API I have an items table with foreign keys from users and units tables
but when I load the table to ObservableCollection then bind it to a datagrid the table normal column load it's data normally into the datagrid excpet for the foreign keys which show nothing but when i insert a break point to see the data inside the ObservableCollection I can see that every thing from Users and Units table is there
private void MainContentsWindow_ContentRendered(object sender, EventArgs e)
{
using (var db2 = new DataContext())
{
var AllItems2 = new ObservableCollection<Model.Items.Item>(db2.Items);
ItemsDataGrid.ItemsSource = AllItems2;
}
}
Users
public class User
{
public User()
{
Id = Guid.NewGuid();
IsActive = false;
}
public Guid Id { get; set; }
public string Name { get; set; }
public string Password { get; set; }
public UserGroup Group { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Items.Item> Items { get; set; } = new List<Items.Item>();
}
public enum UserGroup
{
Administrator = 1,
User,
Unknown
}
base
public class NormalBaseModel : CommonBase
{
public NormalBaseModel()
{
Id = new Guid();
CreateDate = DateTime.Now;
EditDate = null;
}
public Guid Id { get; set; }
public string Notes { get; set; }
public virtual User CreateBy { get; set; }
public DateTimeOffset? CreateDate { get; set; }
public virtual User EditBy { get; set; }
public DateTimeOffset? EditDate { get; set; }
}
items
public class Item : NormalBaseModel
{
public string NameAr { get; set; }
public string NameEn { get; set; }
public int? ManualId { get; set; }
public string Barcode { get; set; }
public byte?[] Image { get; set; }
public virtual Unit Unit { get; set; }
public string MadeIn { get; set; }
public bool IsSerail { get; set; }
public bool IsExpire{ get; set; }
}
Here is a test project on Github
https://github.com/ahmedpiosol/psychic-parakeet.git
https://imgur.com/a/zimd4
When you load your items via EF it needs to create new instances of User and Item. Behind the scenes, EF will call the constructor for each new instance. Your problem is in your constructors:
public User()
{
Id = Guid.NewGuid(); // <- here
}
Your constructor reassigns a new ID each time an instance is created, this will break the referential integrity and cause all sorts of other problems.
Your code doesn't know the difference between creating a new User and recreating a User instance from the database.
I suggest removing the assignments from inside your constructor and placing this either in a static Create method or place wherever you are creating a new User or Item.
p.s. WPF is irrelevant to your problem here.
Fluent API needs to specify foreign key in code, something like
modelBuilder.Entity<Items>()
.HasRequired(o => o.User)
.WithMany(c => c.Items)
.HasForeignKey(o => o.UserId);
I chose to put a complete example of my code in order to demonstrate what I exactly need. Briefly, what I would like to do, is to get a generic code in public override int SaveChanges() that could work to all entities that implements a translation rather than write a one-by-one.
ENTITIES
public partial class EntityOne
{
public long EntityOneId { get; set; }
public string SomeProperty { get; set; }
public virtual ICollection<EntityOneTranslation> EntityOneTranslations { get; set; }
public EntityOne()
{
this.EntityOneTranslations = new HashSet<EntityOneTranslation>();
}
}
public class EntityOneTranslation : EntityTranslation<long, EntityOne>
{
public string LocalizedEntityOneProp1 { get; set; }
public string LocalizedEntityOneProp1 { get; set; }
}
public partial class EntityTwo
{
public long EntityTwoId { get; set; }
public string SomeProperty { get; set; }
public virtual ICollection<EntityTwoTranslation> EntityTwoTranslations { get; set; }
public EntityTwo()
{
this.EntityTwoTranslations = new HashSet<EntityTwoTranslation>();
}
}
public class EntityTwoTranslation : EntityTranslation<long, EntityTwo>
{
public string LocalizedEntityTwoProp1 { get; set; }
public string LocalizedEntityTwoProp2 { get; set; }
}
public class EntityTranslation<TEntityKey, TEntity> : ILanguage
{
[Key, Column(Order = 0), ForeignKey("Entity")]
public TEntityKey EntityKey { get; set; }
[Key, Column(Order = 1), ForeignKey("Language")]
public long LanguageId { get; set; }
public virtual TEntity Entity { get; set; }
public virtual Language Language { get; set; }
}
INTERFACE
public interface ILanguage
{
long LanguageId { get; set; }
}
Here is the target
How would I get the entity navigation property using reflection/or something in order to reuse my code that could work too all entities that has a translation property collection?
I already tried to ask the same thing from 2 other posts, but I didn't give all the info. I guess that's why nobody could give me the expected answer.
Adding new entries over entity navigation property collection
Cast PropertyInfo to Collection
SAVE CHANGES OVERRIDE
public override int SaveChanges()
{
foreach (var entityOneEntry in ChangeTracker.Entries<EntityOne>())
{
if (entityOneEntry.State == EntityState.Added)
{
//Get entity localized properties values of current language.
var currentLanguageEntry = entityOneEntry.Entity.EntityOneTranslations.FirstOrDefault();
var localizedEntityOneProp1 = currentLanguageEntry.LocalizedEntityOneProp1;
var localizedEntityOneProp2 = currentLanguageEntry.LocalizedEntityOneProp2;
//Get all languages but the current one.
var languages = Language.Where(l => l.LanguageId != currentCulture.Key);
//Add missing translations copying the same values.
foreach (var language in languages)
entityOneEntry.Entity.EntityOneTranslations.Add(new EntityOne()
{
LanguageId = language.LanguageId,
LocalizedEntityOneProp1 = localizedEntityOneProp1,
LocalizedEntityOneProp2 = localizedEntityOneProp2
});
}
}
foreach (var entityOneEntry in ChangeTracker.Entries<EntityTwo>())
{
if (entityOneEntry.State == EntityState.Added)
{
//Get entity localized properties values of current language.
var currentLanguageEntry = entityOneEntry.Entity.EntityTwoTranslations.FirstOrDefault();
var localizedEntityTwoProp1 = currentLanguageEntry.LocalizedEntityTwoProp1;
var localizedEntityTwoProp2 = currentLanguageEntry.LocalizedEntityTwoProp2;
//Get all languages but the current one.
var languages = Language.Where(l => l.LanguageId != currentCulture.Key);
//Add missing translations copying the same values.
foreach (var language in languages)
entityOneEntry.Entity.EntityTwoTranslations.Add(new EntityTwo()
{
LanguageId = language.LanguageId,
LocalizedEntityTwoProp1 = localizedEntityTwoProp1,
LocalizedEntityTwoProp2 = localizedEntityTwoProp2
});
}
}
}