Entity Framework Core 2.1. Proper way to update an object with a lot of sub objects - asp.net-core

I'm developing an ASP.NET Core + EF Core application.
I'm struggling a bit (a lot actually) with the update method of the controller which manage the most complex object of my project.
The object looks something like this :
public class CIApplication : ConfigurationItem
{
public String InventoryCollectionQuery { get; set; }
public EnumCCMCIType CCMCIType { get; set; }
//Collection of DeploymentScenario
public virtual ICollection<DeploymentScenario> DeploymentScenarios { get; set; }
//Collection of SoftwareMeteringRules
public virtual ICollection<SoftwareMeteringRule> SoftwareMeteringRules { get; set; }
//Collection of AppLocalPresenceCondition
public virtual ICollection<AppLocalPresenceCondition> AppLocalPresenceConditions { get; set; }
}
And some of the objects in collections can also contains other collections and so, on and so on...
Actually my update methods works (which is below) but is a real pain to maintain when I do some evolutions on my model. As I developed it quite a while ago I'm wondering if there's a better approach to do this. And by better a mean a lot simpler for me :) sometimes I even wonder if it wouldn't be better to delete the object and recreate it.
var cIApplicationInDB = _context.CIApplications
.Include(c => c.Translations)
.Include(c => c.DeploymentScenarios).ThenInclude(d => d.InstallSteps)
.Include(c => c.DeploymentScenarios).ThenInclude(d => d.InstallSteps).ThenInclude(s => s.Translations)
.Include(c => c.DeploymentScenarios).ThenInclude(d => d.InstallSteps).ThenInclude(s => s.InputVariables).ThenInclude(x => x.Translations)
.Include(c => c.DeploymentScenarios).ThenInclude(d => d.InstallSteps).ThenInclude(s => s.OutPutVariables).ThenInclude(x => x.Translations)
.Include(c => c.DeploymentScenarios).ThenInclude(d => d.UninstallSteps)
.Include(c => c.DeploymentScenarios).ThenInclude(d => d.UninstallSteps).ThenInclude(s => s.Translations)
.Include(c => c.DeploymentScenarios).ThenInclude(d => d.UninstallSteps).ThenInclude(s => s.InputVariables).ThenInclude(x => x.Translations)
.Include(c => c.DeploymentScenarios).ThenInclude(d => d.UninstallSteps).ThenInclude(s => s.OutPutVariables).ThenInclude(x => x.Translations)
.Include(c => c.DeploymentScenarios).ThenInclude(d => d.Translations)
.Include(c => c.DeploymentScenarios).ThenInclude(d => d.DetectionMethods)
.Include(c => c.DeploymentScenarios).ThenInclude(d => d.InstallationBehaviorRules)
.Include(c => c.SoftwareMeteringRules)
.Include(c => c.Catalogs)
.Include(c => c.Categories)
.Include(c => c.OwnerCompany)
.Include(c => c.AppLocalPresenceConditions)
.SingleOrDefault(c => c.ID == id);
_context.Entry(cIApplicationInDB).CurrentValues.SetValues(cIApplication);
// Updating Translations
foreach (var upTodateTranslation in cIApplication.Translations)
{
var existingTranslation = cIApplicationInDB.Translations.FirstOrDefault(t => t.Id == upTodateTranslation.Id);
if (existingTranslation == null)
{
cIApplicationInDB.Translations.Add(upTodateTranslation);
}
else
{
_context.Entry(existingTranslation).CurrentValues.SetValues(upTodateTranslation);
}
}
foreach (var ExistingTranslation in cIApplicationInDB.Translations)
{
if (!cIApplication.Translations.Any(t => t.Id == ExistingTranslation.Id))
{
_context.Remove(ExistingTranslation);
}
}
// Updating SoftwareMeteringRules
foreach (var upTodateSoftwareMeteringRule in cIApplication.SoftwareMeteringRules)
{
var existingSoftwareMeteringRule = cIApplicationInDB.SoftwareMeteringRules.FirstOrDefault(t => t.ID == upTodateSoftwareMeteringRule.ID);
if (existingSoftwareMeteringRule == null)
{
cIApplicationInDB.SoftwareMeteringRules.Add(upTodateSoftwareMeteringRule);
}
else
{
_context.Entry(existingSoftwareMeteringRule).CurrentValues.SetValues(upTodateSoftwareMeteringRule);
}
}
foreach (var existingSoftwareMeteringRule in cIApplicationInDB.SoftwareMeteringRules)
{
if (!cIApplication.SoftwareMeteringRules.Any(t => t.ID == existingSoftwareMeteringRule.ID))
{
_context.Remove(existingSoftwareMeteringRule);
}
}
// Updating DeploymentScenarios Graph
foreach (var upToDateDS in cIApplication.DeploymentScenarios)
{
// Look for incoming DS into the actual db CI
var existingDeploymentScenario = cIApplicationInDB.DeploymentScenarios.FirstOrDefault(d => d.ID == upToDateDS.ID);
// can't find DS in existing CI, it is new
if (existingDeploymentScenario == null)
{
// cIApplicationInDB.DeploymentScenarios.
cIApplicationInDB.DeploymentScenarios.Add(upToDateDS);
}
// Look if there are things to update in existing DS
else
{
_context.Entry(existingDeploymentScenario).CurrentValues.SetValues(upToDateDS);
// update DeploymentScenario translations
....
...doing the same way for all objects
// and then after going through all sub collections :
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException e)
{
if (!CIApplicationExists(id))
{
return NotFound();
}
else
{
}
}
catch (Exception e)
{
throw new Exception(e.Message);
}
}
}

Related

How to correctly return asynchronous query result as Task<IAsyncEnumerable<string>>?

I try to return **asynchronous **query result as Task<IAsyncEnumerable>, but can't figure out how to do it correctly. For example everything looks to be ok until I use await keyword.
No error:
`
public async Task<IAsyncEnumerable<string>> GetUserRoles(User user, int userPermisionsId)
{
using (XXX db = new XXX())
{
var userRoles = db.WebAppUserRoles
.Where(x => x.PermisionsId == userPermisionsId)
.Include(x => x.WebAppRole)
.Select(x => new WebAppRole
{
WebAppRoleName = x.WebAppRole.WebAppRoleName
})
.Select(x => x.WebAppRoleName)
.AsAsyncEnumerable();
return userRoles;
}
}
`
I get an error (CS1061'IAsyncEnumerable' does not contains a definition for 'GetAwaiter'...)`:
`
public async Task<IAsyncEnumerable<string>> GetUserRoles(User user, int userPermisionsId)
{
using (XXX db = new XXX())
{
var userRoles = await db.WebAppUserRoles
.Where(x => x.PermisionsId == userPermisionsId)
.Include(x => x.WebAppRole)
.Select(x => new WebAppRole
{
WebAppRoleName = x.WebAppRole.WebAppRoleName
})
.Select(x => x.WebAppRoleName)
.AsAsyncEnumerable();
return userRoles;
}
}
`
Is my method still asynchronous even if I don't use await keyword? I'm new in asynchronous programming topic and I'm bit lost in it...
I was looking for an answer on stackoverflow

How to remove collection of nested table using EF Core

I am working on an application where there are many table linked with each other I want to remove the collection related with sub nested table Here is the implementation I have added but I am in search of better approach as I don't think this is the best one. Here is my code
public async Task AddUpdateProjectCosting(string userId, ProjectCostingDTO model)
{
var estimate = await GetAll().Where(x => x.IsDeleted != true)
.Include(x => x.CustomerAddress)
.Include(x => x.TaxEntity)
.Include(x => x.ProjectAddress)
.Include(x => x.CustomerBillingAddress)
.Include(x => x.CompanyAddress)
.Include(x => x.AdditionalExpenses)
.Include(x => x.EstimateDetails.Where(d => d.IsDeleted != true))
.ThenInclude(x => x.EstimateDetailsSections.Where(s => s.IsDeleted != true))
.ThenInclude(x => x.EstimateCostLineItems.Where(c => c.IsDeleted != true))
.ThenInclude(x => x.TaxEntity).FirstOrDefaultAsync();
if (estimate != null)
{
estimate.UpdatedBy = userId;
estimate.UpdatedDate = DateTime.UtcNow;
estimate.OverHeadPercentage = model.OverHeadPercent;
foreach (var item in estimate.EstimateDetails)
{
if(item.EstimateDetailsSections.Count() > 0)
{
foreach (var detail in item.EstimateDetailsSections)
{
if(detail.EstimateCostLineItems.Count() > 0)
{
foreach (var costLine in detail.EstimateCostLineItems)
{
if (costLine.AdditionalExpenses.Count() > 0) // remove this additional expense....
costLine.AdditionalExpenses = new List<AdditionalExpense>(); /// Here is the additiona expense exists in EstimateCostLine table
}
}
}
}
}
await Change(estimate);
}
}
Here I am getting estimate record which I need to update it as well that is why I am using Include and ThenInclude. There is a collection inside EstimateCostLineItem which I want to remove and add new collection. How can I achieve this by using best approach.
I would suggest to do not load properties which is not used for Update. Also removing items can improved by selecting from database only needed data:
public async Task AddUpdateProjectCosting(string userId, ProjectCostingDTO model)
{
var estimate = await GetAll()
// why there is no appropriate filter?
.Where(x => x.IsDeleted != true)
.FirstOrDefaultAsync();
if (estimate != null)
{
estimate.UpdatedBy = userId;
estimate.UpdatedDate = DateTime.UtcNow;
estimate.OverHeadPercentage = model.OverHeadPercent;
// filter from child to parent
// Correct with actual navigation properties
var toRemove = await context.AdditionalExpenses
.Where(a => a.EstimateCostLineItem.EstimateDetailsSection.EstimateDetail.Estimate.Id == estimate.Id)
.ToListAsync();
context.AdditionalExpenses.RemoveRange(toRemove);
await Change(estimate);
}
}

Breeze: Differences between Entity Framework and NHibernate with many to many

Here is the situation:
WebApi v1
Breeze 1.4.7
EF 5.0/NHibernate 3.3.1
What We want: A many to many exposed as a many to one. A client can have multiple countries and a country can have multiple clients. A ClientCountry entity has been created for that purpose.
My mapping looks like this:
Entity Framework:
modelBuilder.Entity<Client>().HasKey(p => p.Id).Property(p=>p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Client>().Property(p => p.Abbreviation);
modelBuilder.Entity<Client>().Property(p => p.ClientSinceDate).IsRequired();
modelBuilder.Entity<Client>().Property(p => p.ClientUntilDate);
modelBuilder.Entity<Client>().Property(p => p.Name).IsRequired();
modelBuilder.Entity<Client>().Property(p => p.Website);
modelBuilder.Entity<Client>().HasMany(p => p.Contacts).WithRequired(p => p.Client).WillCascadeOnDelete(true);
modelBuilder.Entity<Client>().HasMany(p => p.ClientCountries).WithRequired(p => p.Client).WillCascadeOnDelete(true);
modelBuilder.Entity<Contact>().HasKey(p => p.Id).Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Contact>().Property(p => p.Username);
modelBuilder.Entity<Contact>().HasRequired(p => p.Client);
modelBuilder.Entity<Country>().HasKey(p => p.Id).Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Country>().Property(p => p.ValidFrom);
modelBuilder.Entity<Country>().Property(p => p.ValidTo);
modelBuilder.Entity<Country>().Property(p => p.Code);
modelBuilder.Entity<Country>().Property(p => p.DefaultLabel);
modelBuilder.Entity<Country>().Property(p => p.Description);
modelBuilder.Entity<Country>().Property(p => p.DisplayOrder);
modelBuilder.Entity<ClientCountry>().HasKey(p => p.Id).Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<ClientCountry>().Property(p => p.ValidFrom);
modelBuilder.Entity<ClientCountry>().Property(p => p.ValidTo);
modelBuilder.Entity<ClientCountry>().HasRequired(p => p.Client);
modelBuilder.Entity<ClientCountry>().HasRequired(p => p.Country);
NHibernate:
public class BaseMapping<T> : ClassMapping<T> where T : BaseEntity
{
public BaseMapping()
{
this.Lazy(true);
Id(x => x.Id, map => { map.Generator(Generators.GuidComb); });
}
}
public class ClientMap : BaseMapping<Client>
{
public ClientMap()
{
this.Property(x => x.Name);
this.Property(x => x.Abbreviation);
this.Property(x => x.ClientSinceDate, map => map.NotNullable(true));
this.Property(x => x.ClientUntilDate);
this.Property(x => x.City);
this.Property(x => x.Website);
this.Bag<Department>(x => x.Departments, colmap =>
{
colmap.Key(x => x.Column("ClientId"));
colmap.Inverse(true);
colmap.Cascade(Cascade.All | Cascade.DeleteOrphans);
}, map =>
{
map.OneToMany();
});
this.Bag<ClientCountry>(x => x.ClientCountries, colmap =>
{
colmap.Cascade(Cascade.All | Cascade.DeleteOrphans);
colmap.Key(p => p.Column("ClientId"));
colmap.Inverse(true);
}, map =>
{
map.OneToMany();
});
this.Bag<Contact>(x => x.Contacts, colmap =>
{
colmap.Key(x => x.Column("ClientId"));
colmap.Cascade(Cascade.All | Cascade.DeleteOrphans);
}, map =>
{
map.OneToMany();
});
}
}
public class CountryMap : BusinessRefEntityMapping<Country>
{
public CountryMap()
{
Bag<ClientCountry>(x => x.ClientCountries, colmap =>
{
colmap.Cascade(Cascade.All);
colmap.Key(p => p.Column("CountryId"));
}, map =>
{
map.OneToMany();
});
}
}
public class ClientCountryMap : BaseMapping<ClientCountry>
{
public ClientCountryMap()
{
Property(x => x.ValidFrom);
Property(x => x.ValidTo);
Property(x => x.ClientId, map =>
{
map.Column("ClientId");
map.Insert(false);
map.Update(false);
map.NotNullable(true);
});
Property(x => x.CountryId, map =>
{
map.Column("CountryId");
map.Insert(false);
map.Update(false);
map.NotNullable(true);
});
ManyToOne<Client>(x => x.Client, map =>
{
map.Column("ClientId");
map.Cascade(Cascade.All);
map.Insert(true);
map.Update(true);
map.NotNullable(true);
});
ManyToOne<Country>(x => x.Country, map =>
{
map.Column("CountryId");
map.Cascade(Cascade.All);
map.Insert(true);
map.Update(true);
map.NotNullable(true);
});
}
}
The js code:
$scope.create = function (index) {
var c = $scope.clients[index];
var newClientCountry = breezeService.manager.createEntity('ClientCountry', {
ValidFrom: Date(2013, 01, 01),
ValidTo: Date(2015, 01, 01),
Client: c,
Country: country,
});
breezeService.manager.saveChanges()
.then(function (data) {
$log.info('client created');
})
.fail(function (dat) {
$log.error('save client failed:' + data)
})
}
The issue: With NHibernate, saving a clientcountry results in this error message :"not-null property references a null or transient value CdT.EAI.DAL.ClientCountry.Country". With EF, all works as expected.
Is there something wrong with my code?
So, since there is no feedback yet, here's what we did to make it (more or less) work:
First, to be able to save a new ClientCountry(many to many with exposed junction table), we must do this:
public class ClientCountryMap : BaseMapping<ClientCountry>
{
public ClientCountryMap()
{
Property(x => x.ValidFrom);
Property(x => x.ValidTo);
Property(x => x.ClientId, map =>
{
map.Column("ClientId");
map.Insert(true);
map.Update(true);
map.NotNullable(true);
});
Property(x => x.CountryId, map =>
{
map.Column("CountryId");
map.Insert(true);
map.Update(true);
map.NotNullable(true);
});
ManyToOne<Client>(x => x.Client, map =>
{
map.Column("ClientId");
map.Cascade(Cascade.All);
map.Insert(false);
map.Update(false);
map.NotNullable(true);
});
ManyToOne<Country>(x => x.Country, map =>
{
map.Column("CountryId");
map.Cascade(Cascade.All);
map.Insert(false);
map.Update(false);
map.NotNullable(true);
});
}
}
The difference here is that the insert/update are inverted. Insert(true) and Update(true) must be set on the foreign key mapping and not the association. It differs from the breeze doc on that point.
The second is that Inverse(true) must be set on each collection association or all your collections will be deleted when updating the parent entity.
With that,changes, it seems to work as expected.
ps: It is fixed in the latest version (>1.4.8). The comments above don't apply anymore.

NHibernate and JoinAlias throw exception

I have query in HQL which works good:
var x =_session.CreateQuery("SELECT r FROM NHFolder f JOIN f.DocumentComputedRights r WHERE f.Id = " + rightsHolder.Id + " AND r.OrganisationalUnit.Id=" + person.Id);
var right = x.UniqueResult<NHDocumentComputedRight>();
Basically I receive NHDocumentComputedRight instance.
I've tried to implement the same query in QueryOver. I did this:
var right = _session.QueryOver<NHFolder>().JoinAlias(b => b.DocumentComputedRights, () => cp).Where(h => h.Id == rightsHolder.Id && cp.OrganisationalUnit.Id == person.Id)
.Select(u => cp).List<NHDocumentComputedRight>();
But I get null reference exception.
How can I implement this query in QueryOver?
Update (added mappings) - NHibernate 3.2:
public class FolderMapping: ClassMapping<NHFolder>
{
public FolderMapping()
{
Table("Folders");
Id(x => x.Id, map =>
{
map.Generator(IdGeneratorSelector.CreateGenerator());
});
//more not important properties...
Set(x => x.DocumentComputedRights, v =>
{
v.Table("DocumentComputedRightsFolder");
v.Cascade(Cascade.All | Cascade.DeleteOrphans);
v.Fetch(CollectionFetchMode.Subselect);
v.Lazy(CollectionLazy.Lazy);
}, h => h.ManyToMany());
Version(x => x.Version, map => map.Generated(VersionGeneration.Never));
}
}
public class DocumentComputedRightMapping : ClassMapping<NHDocumentComputedRight>
{
public DocumentComputedRightMapping()
{
Table("DocumentComputedRights");
Id(x => x.Id, map =>
{
map.Generator(IdGeneratorSelector.CreateGenerator());
});
//more not important properties...
ManyToOne(x => x.OrganisationalUnit, map =>
{
map.Column("OrganisationalUnit");
map.NotNullable(false);
map.Cascade(Cascade.None);
});
}
}
public class OrganisationUnitMapping : ClassMapping<NHOrganisationalUnit>
{
public OrganisationUnitMapping()
{
Table("OrganisationalUnits");
Id(x => x.Id, map =>
{
map.Generator(IdGeneratorSelector.CreateGenerator());
});
//more not important properties...
}
}
Thanks
AFAIK criteria/queryOver can only return the entity it was created for (NHFolder in your example) or columns which are set to entity with aliastobean. you could do a correlated subquery instead.
var subquery = QueryOver.Of<NHFolder>()
.JoinAlias(b => b.DocumentComputedRights, () => cp)
.Where(h => h.Id == rightsHolder.Id && cp.OrganisationalUnit.Id == person.Id)
.Select(u => cp.Id);
var right = _session.QueryOver<NHDocumentComputedRight>()
.WithSubquery.Where(r => r.Id).Eq(subquery)
.SingleOrDefault<NHDocumentComputedRight>();
I think you have a problem with the select statement, have you tried something like this:
var right = _session.QueryOver<NHFolder>()
.JoinAlias(b => b.DocumentComputedRights, () => cp)
.Select(x => x.DocumentComputedRights)
.Where(h => h.Id == rightsHolder.Id && cp.OrganisationalUnit.Id == person.Id)
.List<NHDocumentComputedRight>();
This is what is working for me so it should work in you case as well.
I would guess that the main reason behind the problem is the lack of proper overload on the Select method. In reality you would like to write it like this:
.JoinAlias(b => b.DocumentComputedRights, () => cp)
.Select(() => cp)
but the Expression<Func<object>> is not there. Hopefully it's going to be included in the next version.

Fluent Nhibernate Component Prefix

Is there a way to set the column prefix for a component in fluent. For example:
public class SomeClassMap : ClassMap < SomeClass >
{
public SomeClassMap()
{
CreateMap();
}
private void CreateMap()
{
WithTable("Class");
Id(x => x.Id).GeneratedBy.Guid();
Map(x => x.Name).WithLengthOf(100);
Component<SomeComponent>(x => x.somecomponent, m =>
{
m.Map(x => x.Name).SetAttribute("column", "SomeComponentName");
m.Map(x => x.Summary).SetAttribute("column", "SomeComponentSummary");
.... etc ...
}
);
Is there a way to set "SomeComponent" prefixes instead of having to define them in a SetAttribute?
There's nothing implicit. AutoMapping does it, but not regular mapping. I've created an issue so you can track the status of this.
There is some good information here: http://nhforge.org/blogs/nhibernate/archive/2008/09/06/a-fluent-interface-to-nhibernate-part-2-value-objects.aspx that seems to be what you are wanting to do.
In particular the Action method demonstrated in this sample:
public class EmployeeMap : ClassMap<Employee>
{
private Action<ComponentPart<Address>> MapAddress(string columnPrefix)
{
return a =>
{
a.Map(x => x.AddressLine1, columnPrefix + "AddressLine1");
a.Map(x => x.AddressLine2, columnPrefix + "AddressLine2");
a.Map(x => x.PostalCode, columnPrefix + "PostalCode");
a.Map(x => x.City, columnPrefix + "City");
a.Map(x => x.Country, columnPrefix + "Country");
};
}
public EmployeeMap()
{
Id(x => x.Id);
Map(x => x.FirstName).CanNotBeNull().WithLengthOf(20);
Map(x => x.LastName).CanNotBeNull().WithLengthOf(20);
Component<Address>(x => x.HomeAddress, MapAddress("Home_"));
Component<Address>(x => x.WorkAddress, MapAddress("Work_"));
}
}