Extending a EF Entity for business logic throws a mapping exception - c#

I have a model in our Database ImportError. I have a class the extends this model, AssessmentImportError. AssessmentImportError does not map to a table. It has a [NotMapped] attribute on it. When I try to select or insert a ImportError (not an AssessmentImportError...) I get the following exception:
Exception:
An exception of type 'System.Exception' occurred in EntityFramework.MappingAPI.dll but was not handled in user code
Additional information: Type 'DAL.SharedModels.AssessmentImportError' is not found in context 'DAL.HTTrans.Model.HTTransDB'
The models:
[NotMapped]
public class AssessmentImportError : ImportError
{
public string APN { get; set; }
}
public partial class ImportError : Frameworks.Interfaces.IImportError
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long ErrorId { get; set; }
[Required]
[MaxLength(200)]
[Index("IX_TableName_RecordId", 1)]
public string TableName { get; set; }
// ... deleted code
}
Bulk Insert:
private static void SaveErrors(List<IImportError> errors, int batchID)
{
// Casting from IImportError to ImportError. They are already an ImportError though.
List<ImportError> castedErrors = errors.Select(e => (ImportError)e).ToList();
using (var db = new HTTransDB())
{
foreach (var e in castedErrors)
{
e.BatchId = batchID;
}
db.BulkInsert(castedErrors);
errors.Clear();
}
}
EDIT:
If I change db.BulkInsert(castedErrors) to db.ImportErrors.AddRange(castedErors) I no longer have an issue. This appears to be a bug within the BulkInsert Nuget package's Mapping API.

This cast:
errors.Select(e => (ImportError)e)
will still return a list of AssessmentImportError which are not mapped to the DB model.
What you should do is maybe an extension method (or a service) which maps from AssessmentImportError to the desired ImportError like this:
List<ImportError> castedErrors = errors.Select(e => e.To<ImportError>()).ToList();
where
To< ImportError >() ===> returns new ImportError object
is the extension method.

Related

Related data not saved when added after creation with EF Core 6

I have POCO objects that are exposed through a repository that uses EF Core 6 to access a database. I can persist "parent" objects to the database and related data that is added to the parent object before creating is persisted successfully as well. However, when trying to add children (SingleSimulationResult objects) to a parent object (SingleSimulation objects) after it has been created, the children are not persisted to the database.
Here is the code that tries to add and save children to the parent object.
singleSim.AddResultsToSimulation(allResults);
Console.WriteLine($"# results: {singleSim.Results.Count}"); // # results: 2
await scopedRepository.Save();
var test = await scopedRepository.GetById(singleSim.Id);
Console.WriteLine($"# results test: {test.Results.Count}"); // # results: 0
SingleSimulation class (BaseEntity just defines an Id property):
public class SingleSimulation : BaseEntity, IAggregateRoot
{
public string Name { get; private set; }
public string Description { get; private set; }
public double Capital { get; private set; }
public List<List<double>> Returns { get; private set; }
private readonly List<SingleSimulationStrategy> _strategies = new List<SingleSimulationStrategy>();
public IReadOnlyCollection<SingleSimulationStrategy> Strategies => _strategies.AsReadOnly();
private List<SingleSimulationResult> _results = new List<SingleSimulationResult>();
public IReadOnlyCollection<SingleSimulationResult> Results => _results.AsReadOnly();
public SingleSimulation()
{
}
public SingleSimulation(string name, string description, double capital, List<List<double>> returns, List<SingleSimulationStrategy> strategies)
{
Name = name;
Description = description;
Capital = capital;
Returns = returns;
_strategies = strategies;
}
public void AddResultsToSimulation(List<SingleSimulationResult> results)
{
if (_results is null)
return;
foreach (var result in results)
{
_results.Add(result);
}
}
}
Repository class:
public class SingleSimulationRepository : ISingleSimulationRepository
{
private SimulationDbContext _dbContext;
public SingleSimulationRepository(SimulationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task Add(SingleSimulation entity)
{
await _dbContext.AddAsync(entity);
}
public async Task<SingleSimulation> GetById(int id)
{
return await _dbContext.SingleSimulations.FindAsync(id);
}
...
public async Task Save()
{
await _dbContext.SaveChangesAsync();
}
}
DbContext:
public class SimulationDbContext : DbContext
{
public SimulationDbContext(DbContextOptions<SimulationDbContext> options)
: base(options)
{
}
public DbSet<SingleSimulation> SingleSimulations { get; set; }
public DbSet<SingleSimulationResult> SingleSimulationResults { get; set; }
public DbSet<SingleSimulationStrategy> SingleSimulationStrategies { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Seed data and custom conversion functions
}
}
Here's what I have tried (to no avail):
Using Fluent API to configure One-to-Many relationship for Results (using .HasMany()).
modelBuilder.Entity<SingleSimulation>()
.HasMany(x => x.Results)
.WithOne();
Using AddRange() to add result objects to the DB before adding them to the parent and finally saving to DB (SaveChangesAsync).
Using Attach() to start tracking result objects before adding them to the parent.
Using Include() when loading the parent object from the database before adding children and trying to save them.
It feels like I'm missing something small, but after scouring the docs and other sources I cannot find the problem. What do I need to do to get children added to the parent after the parent has already been created to actually save to the DB?
After debugging by printing the EF Core change tracker's LongView, I noticed that no changes are detected on the object (even if changing a simple string property). It turns out the problem was that the singleSim object I was modifying was returned from a different dbContext than the one used by the scopedRepository.
The model setup wasn't the problem after all. Even without the Fluent API config the setup works as intended (even with the read only collections and private backing fields).

Do we have to explicitly add to db context?

public class Practice
{
public List<Participation> Participation { get; set; }
}
public class Participation
{
public string Id { get; set; }
public virtual Practice Practice { get; set; }
}
public void test()
{
var practice = _ctx.Practice.SingleOrDefault(p => p.Id == practiceId);
practice.Participations.AddRange(NewParticipations);
_ctx.Participation.AddRange(NewParticipations)
await _ctx.SaveChangesAsync();
}
If I have the above, would I need the 3rd line in the test function to save new participations or would the practice.Participations.AddRange() handle that implicitly?
practice.Participations.AddRange should be enough.
If you reference a new entity from the navigation property of an entity that is already tracked by the context, the entity will be discovered and inserted into the database.
source: https://learn.microsoft.com/en-us/ef/core/saving/related-data#adding-a-related-entity
You can observe it like so...
var practice = _ctx.Practice.SingleOrDefault(p => p.Id == practiceId);
practice.Participations.AddRange(NewParticipations);
Debug.WriteLine(_ctx.Participation.Count()); //note count
await _ctx.SaveChangesAsync();
Debug.WriteLine(_ctx.Participation.Count()); //count increased
You should be able to add the new data to the database either way. If you added through the context, you would need to set the foreign key in the NewParticipations objects yourself, so that a link would exist to the Practice object.

Entity Framework Core 2.0 Many to Many Inserts before primary key is generated

I'm trying to create an entity object that has many to many relationships with other entities. The relationships are indicated as follows.
public class Change {
// Change Form Fields
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ChangeId { get; set; }
public string ChangeTitle { get; set; }
public string ChangeType { get; set; }
public DateTime DateSubmitted { get; set; }
public DateTime TargetDate { get; set; }
//Many to Many Collections
public virtual ICollection<Change_CriticalBankingApp> Change_CriticalBankingApps { get; set; } = new List<Change_CriticalBankingApp>();
public virtual ICollection<Change_ImpactedBusiness> Change_ImpactedBusinesses { get; set; } = new List<Change_ImpactedBusiness>();
public virtual ICollection<Change_ImpactedService> Change_ImpactedServices { get; set; } = new List<Change_ImpactedService>();
public virtual ICollection<Change_TestStage> Change_TestStages { get; set; } = new List<Change_TestStage>();
public virtual ICollection<Change_TypeOfChange> Change_TypeOfChanges { get; set; } = new List<Change_TypeOfChange>();
And the DbContext set up is as follows
public class ChangeContext : DbContext {
public ChangeContext(DbContextOptions<ChangeContext> options) : base(options) {
Database.Migrate();
}
public DbSet<Change> Change { get; set; }
public DbSet<TestStage> TestStage { get; set; }
public DbSet<TypeOfChange> TypeOfChange { get; set; }
public DbSet<CriticalBankingApp> CriticalBankingApp { get; set; }
public DbSet<ImpactedBusiness> ImpactedBusiness { get; set; }
public DbSet<ImpactedService> ImpactedService { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Change_CriticalBankingApp>().HasKey(t => new { t.ChangeId, t.CriticalBankingAppId });
modelBuilder.Entity<Change_ImpactedBusiness>().HasKey(t => new { t.ChangeId, t.ImpactedBusinessId });
modelBuilder.Entity<Change_ImpactedService>().HasKey(t => new { t.ChangeId, t.ImpactedServiceId });
modelBuilder.Entity<Change_TestStage>().HasKey(t => new { t.ChangeId, t.TestStageId });
modelBuilder.Entity<Change_TypeOfChange>().HasKey(t => new { t.ChangeId, t.TypeOfChangeId });
}
}
Where I start running into problems is I'm not generating an Id using Entity Framework, the primary key is an identity in SQL Server 2012 and I get that back once the insert is completed, as opposed to using a GUID (which I've read pretty much everywhere is super frowned upon in the DBA world).
So what ends up happening is I either try and do the insert and it tries to insert the many to many relationships with changeId in the junction table being null (because it isn't generated yet) or when I try what I have below to do an insert and an update in one post method. It errors out because the ChangeId key value is already being tracked. Here is what I'm attempting below.
Controller method
public IActionResult CreateChange([FromBody] ChangeModel change) {
if (change == null) {
return BadRequest();
}
//Remove many to many from Change to insert without them (as this can't be done until primary key is generated.
List<Change_CriticalBankingAppModel> criticalApps = new List<Change_CriticalBankingAppModel>();
criticalApps.AddRange(change.Change_CriticalBankingApps);
List<Change_ImpactedBusinessModel> impactedBusinesses = new List<Change_ImpactedBusinessModel>();
impactedBusinesses.AddRange(change.Change_ImpactedBusinesses);
List<Change_ImpactedServiceModel> impactedServices = new List<Change_ImpactedServiceModel>();
impactedServices.AddRange(change.Change_ImpactedServices);
List<Change_TestStageModel> testStages = new List<Change_TestStageModel>();
testStages.AddRange(change.Change_TestStages);
List<Change_TypeOfChangeModel> changeTypes = new List<Change_TypeOfChangeModel>();
changeTypes.AddRange(change.Change_TypeOfChanges);
change.Change_CriticalBankingApps.Clear();
change.Change_ImpactedBusinesses.Clear();
change.Change_ImpactedServices.Clear();
change.Change_TestStages.Clear();
change.Change_TypeOfChanges.Clear();
//Map Change model to change entity for inserting
var changeEntity = Mapper.Map<Change>(change);
_changeRepository.AddChange(changeEntity);
if (!_changeRepository.Save()) {
throw new Exception("Creating change failed on save.");
}
var changetoReturn = Mapper.Map<ChangeModel>(changeEntity);
//Iterate through Many to many Lists to add generated changeId
foreach (var criticalApp in criticalApps) {
criticalApp.ChangeId = changetoReturn.ChangeId;
}
foreach (var impactedBusiness in impactedBusinesses) {
impactedBusiness.ChangeId = changetoReturn.ChangeId;
}
foreach (var impactedService in impactedServices) {
impactedService.ChangeId = changetoReturn.ChangeId;
}
foreach (var testStage in testStages) {
testStage.ChangeId = changetoReturn.ChangeId;
}
foreach (var changeType in changeTypes) {
changeType.ChangeId = changetoReturn.ChangeId;
}
//Add many to many lists back to change to update
changetoReturn.Change_CriticalBankingApps = criticalApps;
changetoReturn.Change_ImpactedBusinesses = impactedBusinesses;
changetoReturn.Change_ImpactedServices = impactedServices;
changetoReturn.Change_TestStages = testStages;
changetoReturn.Change_TypeOfChanges = changeTypes;
changeEntity = Mapper.Map<Change>(changetoReturn);
_changeRepository.UpdateChange(changeEntity);
if (!_changeRepository.Save()) {
throw new Exception("Updating change with many to many relationships failed on save.");
}
changetoReturn = Mapper.Map<ChangeModel>(changeEntity);
return CreatedAtRoute("GetChange",
new { changeId = changetoReturn.ChangeId },
changetoReturn);
}
Relevant Repository methods
public Change GetChange(int changeId) {
return _context.Change.FirstOrDefault(c => c.ChangeId == changeId);
}
public void AddChange(Change change) {
_context.Change.Add(change);
}
public void UpdateChange(Change change) {
_context.Change.Update(change);
}
public bool ChangeExists(int changeId) {
return _context.Change.Any(c => c.ChangeId == changeId);
}
I encounter this error on the update attempt.
I understand that if I were to have entity framework generate the guid instead of having the database generate the identity int that I would have a much easier time with this but a requirement for this project is to not use Guid's.
Any help on how to successfully process this would be greatly appreciated.
EDIT: In case it helps, here is the http post I'm using with postman.
{
"changeTitle": "Test",
"changeType": "Test",
"dateSubmitted": "02/12/2018",
"targetDate": "02/12/2018",
"change_CriticalBankingApps": [
{
"criticalBankingAppId" : 1,
"description" : "Very critical"
},
{
"criticalBankingAppId" : 2,
"description" : "Moderately critical"
}
],
"change_impactedBusinesses": [
{
"ImpactedBusinessId" : 1
},
{
"ImpactedBusinessId" : 2
}
]
}
The error you are getting has nothing to do with the guid vs db identity.
You are getting it because you are:
Fetching an entity from the database
Creating new entity (not tracked) from within your controller (the mapper does this)
Try to update the entity that is not tracked by entity framework
The update will try to add the entity to the EF repository, but will fail because it already contains an entity with the given ID.
If you plan to make changes to an entity, you need to make sure entity framework tracks the entity prior to calling the update method.
If EF does not track your entity, it does not know which fields have been updated (if any).
Edit:
If you want to get rid of the error, you could detach your original entity. Make sure you do it prior to mapping the changetoReturn back into your changeEntity.
dbContext.Entry(entity).State = EntityState.Detached;
But since your new entity won't be tracked, I don't think anything will be updated (EF does not know what has been changed).
Edit 2:
Also take a look at this to get your changes back into your original entity.
Change this:
changeEntity = Mapper.Map<Change>(changetoReturn);
Into this:
Mapper.Map(changetoReturn, changeEntity);
Using Automapper to update an existing Entity POCO
add new entities via joint table...that way, entities are tracked both in the joint table and their individual respective tables
Ok, whether this is an elegant solution is up for debate, but I was able to detach the entity state from changeEntity after doing the initial insert as follows
_changeRepository.AddChange(changeEntity);
_changecontext.Entry(changeEntity).State = EntityState.Detached;
Then after reattaching all of the many to many lists back to changeToReturn, I created a new Change entity and added that entity state, and updated on that as follows.
var newChangeEntity = Mapper.Map<Change>(changeToReturn);
_changecontext.Entry(newChangeEntity).State = EntityState.Added;
_changeRepository.UpdateChange(newChangeEntity);
Then I returned this mapped back to a view model.
It seems hacky and perhaps through a deeper understanding of entity framework I'll discover a much better way of going about this but this works for now.

Can these models be represented in EF7?

I'm trying to use some classes from another assembly in my own project as entities that I can persist using EF7, rather than writing a series of very similar classes that are more database-friendly.
Simplified versions look like this:
interface IMediaFile
{
string Uri { get; }
string Title { get; set; }
}
class CMediaFile : IMediaFile
{
public CMediaFile() { }
public string Uri { get; set; }
public string Title { get; set; }
}
//The following types are in my project and have full control over.
interface IPlaylistEntry
{
IMediaFile MediaFile { get; }
}
class CPlaylistEntry<T> : IPlaylistEntry where T : IMediaFile
{
public CPlaylistEntry() { }
public T MediaFile { get; set; }
}
There are multiple implementations of IMediaFile, I am showing only one. My PlaylistEntry class takes a generic argument to enable different traits for those various implementations, and I just work with the IPlaylistEntry.
So I've started to model it like so:
var mediaFile = _modelBuilder.Entity<CMediaFile>();
mediaFile.Key(e => e.Uri);
mediaFile.Index(e => e.Uri);
mediaFile.Property(e => e.Title).MaxLength(256).Required();
var mediaFilePlaylistEntry = _modelBuilder.Entity<CPlaylistEntry<CMediaFile>>();
mediaFilePlaylistEntry.Key(e => e.MediaFile);
mediaFilePlaylistEntry.Reference(e => e.MediaFile).InverseReference();
As a simple test, I ignore the CPlaylistEntry<> and just do:
dbContext.Set<CMediaFile>().Add(new CMediaFile() { Uri = "irrelevant", Title = "" });
dbContext.SaveChanges()
This throws:
NotSupportedException: The 'MediaFile' on entity type 'CPlaylistEntry' does not have a value set and no value generator is available for properties of type 'CMediaFile'. Either set a value for the property before adding the entity or configure a value generator for properties of type 'CMediaFile'`
I don't even understand this exception, and I don't see why CPlaylistEntry is appearing when I'm only trying to store a CMediaFile entity. I'm guessing this is related to my model definition - specifically defining the primary key of the CPlaylistEntry as not a simple type, but a complex type - another entity. However I would expect EF to be smart enough to work out that it all boils down to a string Uri, because that complex type has its own primary key declared already, and I have declared the property as a foreign key to that type.
Is it possible to model these classes in EF without radically redesigning them to look closer to what corresponding database tables might be? I've worked with EF6 database-first in the past, so this is my first attempt into a code-first pattern, and I'm really hoping that I can isolate the mess that a database might look like to just my model definition, and keep "clean" classes that I interact with in .NET.
If more explanation of these types and their relationship is required, just ask - I'm attempting to keep this brief.
Doubt this is currently supported (unsure if it eventually will or not).| I've tried to recreate your model with slight changes and when trying to create the database I get:
System.NotSupportedException: The property 'PlaylistEntry`1MediaFile'
cannot be mapped because it is of type 'MediaFile' which is currently
not supported.
Update 1
I think that the fact that you are putting MediaFile as a key is creating problems. I've done a few changes to your model. I hope this will not break anything negative on your end:
public interface IPlaylistEntry<T>
where T : IMediaFile
{
T MediaFile { get; set; }
}
public class PlaylistEntry<T> : IPlaylistEntry<T>
where T : IMediaFile
{
public int Id { get; set; }
public string PlaylistInfo { get; set; } //added for testing purposes
public T MediaFile { get; set; }
}
Mappings:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ForSqlServer().UseIdentity();
builder.Entity<MediaFile>().ForRelational().Table("MediaFiles");
builder.Entity<MediaFile>().Key(e => e.Uri);
builder.Entity<MediaFile>().Index(e => e.Uri);
builder.Entity<MediaFile>().Property(e => e.Title).MaxLength(256).Required();
builder.Entity<PlaylistEntry<MediaFile>>().ForRelational().Table("MediaFileEntries");
builder.Entity<PlaylistEntry<MediaFile>>().Key(e => e.Id);
builder.Entity<PlaylistEntry<MediaFile>>().Reference(e => e.MediaFile).InverseReference();
}
Usage:
var mediaFile = new MediaFile() {Uri = "irrelevant", Title = ""};
context.Set<MediaFile>().Add(mediaFile);
context.SaveChanges();
context.Set<PlaylistEntry<MediaFile>>().Add(new PlaylistEntry<MediaFile>
{
MediaFile = mediaFile,
PlaylistInfo = "test"
});
context.SaveChanges();
This works and saves the correct data to the database.
You can retrieve the data using:
var playlistEntryFromDb = context.Set<PlaylistEntry<MediaFile>>()
.Include(plemf => plemf.MediaFile).ToList();
Update 2
Since you do not want to have an identity as key, you can add a Uri property to your playlistentry class that will be used for the relationship between PlaylistEntry and MediaFile.
public class PlaylistEntry<T> : IPlaylistEntry<T>
where T : IMediaFile
{
public string Uri { get; set; }
public string PlaylistInfo { get; set; }
public T MediaFile { get; set; }
}
Here is what the mapping in this case would look like:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<MediaFile>().ForRelational().Table("MediaFiles");
builder.Entity<MediaFile>().Key(e => e.Uri);
builder.Entity<MediaFile>().Index(e => e.Uri);
builder.Entity<MediaFile>().Property(e => e.Title).MaxLength(256).Required();
builder.Entity<PlaylistEntry<MediaFile>>().ForRelational().Table("MediaFileEntries");
builder.Entity<PlaylistEntry<MediaFile>>().Key(e => e.Uri);
builder.Entity<PlaylistEntry<MediaFile>>().Reference(e => e.MediaFile).InverseReference().ForeignKey<PlaylistEntry<MediaFile>>(e => e.Uri);
}
Usage to insert data stays the same:
var mediaFile = new MediaFile() { Uri = "irrelevant", Title = "" };
context.Set<MediaFile>().Add(mediaFile);
context.SaveChanges();
context.Set<PlaylistEntry<MediaFile>>().Add(new PlaylistEntry<MediaFile>
{
MediaFile = mediaFile,
PlaylistInfo = "test"
});
context.SaveChanges();
This code above will put "irrelevant" in the PlaylistEntry Uri property since it is used as the foreign key.
And to retrieve data:
var mediaFiles = context.Set<PlaylistEntry<MediaFile>>().Include(x => x.MediaFile).ToList();
The join will occur on the Uri field in both tables.

nhibernate mapping.JoinedSubClass(keyColumn) does not respect keyColumn

I'm using Fluent-NHibernate and attempting to persist an object hierarchy using the table per subclass method:
public class AbstractProduct
{
public int ProductId { get; set; }
public string Name { get; set; }
}
public class SingleProduct : AbstractProduct
{
public int SingleProductId { get; set; }
public string SomeField { get; set; }
}
when saving an object
var singleProduct = new SingleProduct();
session.SaveOrUpdate(singleProduct);
I get this error:
NHibernate.Exceptions.GenericADOException: could not insert: [FluentNHibernateSubClassTest.SingleProduct#3][SQL: INSERT INTO SingleProductData (Field1, AbstractProduct_id) VALUES (?, ?)] ---> System.Data.SqlClient.SqlException: Invalid column name 'AbstractProduct_id'.
despite having the following overrides:
public class AbstractProductOverrides : IAutoMappingOverride<AbstractProduct>
{
public void Override(AutoMapping<AbstractProduct> mapping)
{
mapping.Id(x => x.ProductId).Column("ProductId");
//this mapping provided to illustrate the overrides are picked up
mapping.Table("ProductsData");
mapping.JoinedSubClass<SingleProduct>("ProductId");//ignored??
}
}
public class SingleProductOverrides : IAutoMappingOverride<SingleProduct>
{
public void Override(AutoMapping<SingleProduct> mapping)
{
mapping.Id(x => x.SingleProductId);
mapping.Table("SingleProductData");
mapping.Map(x => x.SomeField).Column("Field1");
}
}
It doesn't appear to matter what column name I supply to JoinedSubClass it ignores it and uses AbstractProduct_id instead.
How can I tell nhibernate the key column is ProductId and not AbstractProduct_id?
I have a test project demonstrating the issue available here (you need to create the db)
UPDATE
I've got around this by providing the following convention:
public class JoinedSubclassConvention : IJoinedSubclassConvention
{
public void Apply(IJoinedSubclassInstance instance)
{
if (instance.EntityType == typeof(SingleProduct))
instance.Key.Column(("ProductId"));
}
}
which works but feels like its the wrong way or a hack.
mapping.Id in SingleProductOverrides is flawed. Subclasses don't have their own id, they inherit the Id from their base classes. Even mapping.JoinedSubClass<SingleProduct>("ProductId"); is redundant (probably ignored) if SingleProduct is automapped as well (it is as seen from the Override for it). JoinedSubclassConvention is the right way to do this.

Categories