Instead of using Breeze server side to save JObject, I'm using a dummy contextprovider to extract the EntityMaps and then performing custom validations on each entity and saving them myself. If the save succeeds, how do I reconstruct the SaveResult object to return back to the client so that BreezeJS client knows about my changes?
Currently I'm returning the following SaveResult:
// Using example here (https://github.com/Breeze/breeze.js.samples/issues/33)
// to extract EntityMaps from JObject.
// The return result is a Dictionary<Type, EntityInfo>.
var entityMaps = SaveBundleToSaveMap.Convert(saveBundle);
// ... Code to save entities to DB
// SaveResult to be returned to the client.
return new SaveResult()
{
Entities = entityMaps.SelectMany(innerEi => innerEi.Value.Select(ie => ie.Entity)).ToList<object>(),
Errors = null,
KeyMappings = new List<KeyMapping>()
};
How do I construct the KeyMapping list for single primary keys? How do I construct the KeyMapping for composite keys?
After some trial and error, I found that the SaveResult.KeyMapping list contain only the old and new keys generated from inserted entities. This makes sense because the client has the key of updates and do not need to worry about deleted entities.
To construct the SaveResult.KeyMapping list, first need to set the EntityInfo.AutoGeneratedKey.TempValue of each insert entity to the key sent from the client side, save the entities to get the new keys, and then create KeyMapping list and return back to the client.
1.Loop the entityMaps and set the TempValue for each EntityInfo to the key/id sent from the client side.
// Extract out from loop to make it more readable.
Action<EntityInfo> processEntityInfo = (ei) =>
{
if (ei.EntityState == EntityState.Added && (ei.AutoGeneratedKey != null && ei.AutoGeneratedKey.AutoGeneratedKeyType == AutoGeneratedKeyType.Identity))
{
var entity = ei.Entity;
var tempValue = ei.AutoGeneratedKey.Property.GetValue(entity);
ei.AutoGeneratedKey.TempValue = tempValue;
}
};
entityMaps.ToList().ForEach(map => map.Value.ForEach(ei => processEntityInfo(ei)));
2.Save the entities by calling the the Context.SaveChanges() on the EntityFramework Context. This will generate keys for all the inserted entities.
3.Loop through the saved entities in the entity maps and construct and return the KeyMapping list.
return entityMaps.SelectMany(entityMap =>
entityMap.Value
.Where(entityInfo => entityInfo.EntityState == EntityState.Added && (entityInfo.AutoGeneratedKey != null && entityInfo.AutoGeneratedKey.AutoGeneratedKeyType == AutoGeneratedKeyType.Identity))
.Select(entityInfo => new KeyMapping
{
EntityTypeName = entityInfo.Entity.GetType().FullName,
RealValue = entityInfo.AutoGeneratedKey.Property.GetValue(entityInfo.Entity),
TempValue = entityInfo.AutoGeneratedKey.TempValue
}));
Related
I have my database table named 'JobInfos' in SQL Server which contains many columns.
JobID - (int) auto populates incrementing value when data added
OrgCode - (string)
OrderNumber - (int)
WorkOrder - (int)
Customer - (string)
BaseModelItem - (string)
OrdQty - (int)
PromiseDate - (string)
LineType -(string)
This table gets written to many times a day using a Blazor application with Entity Framework and CSVHelper. This works perfectly. All rows from the CSV file are added to the database.
if (fileExist)
{
using (var reader = new StreamReader(#path))
using (var csv = new CsvReader(reader, config))
{
var records = csv.GetRecords<CsvRow>().Select(row => new JobInfo()
{
OrgCode = row.OrgCode,
OrderNumber = row.OrderNumber,
WorkOrder = row.WorkOrder,
Customer = row.Customer,
BaseModelItem = row.BaseModelItem,
OrdQty = row.OrdQty,
PromiseDate = row.PromiseDate,
LineType = row.LineType,
});
using (var db = new ApplicationDbContext())
{
while (!reader.EndOfStream)
{
if (lineNumber != 0)
{
db.AddRange(records.ToList());
db.SaveChanges();
}
lineNumber++;
}
NavigationManager.NavigateTo("/", true);
}
}
As these multiple CSV files can contain rows that may already be in the database table, I am getting duplicate records when the table is read from, which causes the users to delete all the newer duplicate rows manually to only keep the original entry.
I have no control over the CSV files or their creation. I am trying to only add rows that contain new data based on the WorkOrder number which can not be the same as any others.
I found another post here on StackOverflow which helps but I am stuck with a remaining error I can't figure out.
The Helpful post
I changed my code here...
if (lineNumber != 0)
{
var recordworkorder = records.Select(x => x.WorkOrder).ToList();
var workordersindb = db.JobInfos.Where(x => recordworkorder.Contains(x.WorkOrder)).ToList();
var workordersNotindb = records.Where(x => !workordersindb.Contains(x.WorkOrder));
db.AddRange(records.ToList(workordersNotindb));
db.SaveChanges();
}
but this line...
var workordersNotindb = records.Where(x => !workordersindb.Contains(x.WorkOrder));`
throws an error at the end (x.WorkOrder) - CS1503 Argument 1: cannot convert from 'int' to 'DepotQ4.Data.JobInfo'
WorkOrder is an int
JobID is the Primary Key and an int
Every record in the table must have a unique WorkOrder
I am not sure what I am not seeing. Could use some help here please?
Your variable workordersindb is a List<JobInfo>. So when you try to select from records.Where(x => !workordersindb.Contains(x.WorkOrder)) you are trying to match the list of JobInfo in workordersindb to the int of x.WorkOrder. workordersindb needs to be a List<int> in order to be able to use it with the Contains. records would have had the same issue, but you solved it by creating the variable recordworkorder and using records.Select(x => x.WorkOrder) to get a List<int>.
if (lineNumber != 0)
{
var recordworkorder = records.Select(x => x.WorkOrder).ToList();
var workordersindb = db.JobInfos.Where(x => recordworkorder.Contains(x.WorkOrder)).Select(x => x.WorkOrder).ToList();
var workordersNotindb = records.Where(x => !workordersindb.Contains(x.WorkOrder));
db.JobInfos.AddRange(workordersNotindb);
db.SaveChanges();
}
I sync data from an api and detect if an insert or update is necessary.
From time to time I receive DbUpdateExceptions and then fallback to single insert/update + savechanges instead of addrange/updaterange + savechanges.
Because single entities are so slow I wanted to only remove the failing entity from changetracking and try to save it all again, but unfortunately mssql returns all entities instead of only the one that is failing in DbUpdateException.Entries.
Intellisense tells me
Gets the entries that were involved in the error. Typically this is a single entry, but in some cases it may be zero or multiple entries.
Interestingly this is true if I try it on a mysql server. There only one entity is returned, but mssql returns all, which makes it impossible for me to exclude only the failing one.
Is there any setting to change mssql behaviour?
Both mysql and mssql are azure hosted resources.
Here an example:
var addList = new List<MyEntity>();
var updateList = new List<MyEntity>();
//load existing data from db
var existingData = Context.Set<MyEntity>()
.AsNoTracking()
.Take(2).ToList();
if (existingData.Count < 2)
return;
//addList
addList.Add(new MyEntity
{
NotNullableProperty = "Value",
RequiredField1 = Guid.Empty,
RequiredField2 = Guid.Empty,
});
addList.Add(new MyEntity
{
NotNullableProperty = "Value",
RequiredField1 = Guid.Empty,
RequiredField2 = Guid.Empty,
});
addList.Add(existingData.ElementAt(0)); //this should fail due to duplicate key
addList.Add(new MyEntity
{
NotNullableProperty = "Value",
RequiredField1 = Guid.Empty,
RequiredField2 = Guid.Empty,
});
//updateList
existingData.ElementAt(1).NotNullableProperty = null; //this should fail due to invalid value
updateList.Add(existingData.ElementAt(1));
//save a new entity that should fail
var newKb = new MyEntity
{
NotNullableProperty = "Value",
RequiredField1 = Guid.Empty,
RequiredField2 = Guid.Empty,
};
Context.Add(newKb);
Context.SaveChanges();
newKb.NotNullableProperty = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; //this should fail due to length
updateList.Add(newKb);
try
{
if (addList.IsNotNullOrEmpty())
context.Set<MyEntity>().AddRange(addList);
if (updateList.IsNotNullOrEmpty())
context.Set<MyEntity>().UpdateRange(updateList);
context.SaveChanges();
}
catch (DbUpdateException updateException)
{
//updateException.Entries contains all entries, that were added/updated although only three should fail
}
I have this function and it is working perfectly
public DemandeConge Creat(DemandeConge DemandeConge)
{
try
{
var _db = Context;
int numero = 0;
//??CompanyStatique
var session = _httpContextAccessor.HttpContext.User.Claims.ToList();
int currentCompanyId = int.Parse(session[2].Value);
numero = _db.DemandeConge.AsEnumerable()
.Where(t => t.companyID == currentCompanyId)
.Select(p => Convert.ToInt32(p.NumeroDemande))
.DefaultIfEmpty(0)
.Max();
numero++;
DemandeConge.NumeroDemande = numero.ToString();
//_db.Entry(DemandeConge).State = EntityState.Added;
_db.DemandeConge.Add(DemandeConge);
_db.SaveChanges();
return DemandeConge;
}
catch (Exception e)
{
return null;
}
}
But just when i try to insert another leave demand directly after inserting one (without waiting or refreshing the page )
An error appears saying that this new demand.id exists
I think that i need to add refresh after saving changes?
Any help and thanks
Code like this:
numero = _db.DemandeConge.AsEnumerable()
.Where(t => t.companyID == currentCompanyId)
.Select(p => Convert.ToInt32(p.NumeroDemande))
.DefaultIfEmpty(0)
.Max();
numero++;
Is a very poor pattern. You should leave the generation of your "numero" (ID) up to the database via an Identity column. Set this up in your DB (if DB First) and set up your mapping for this column as DatabaseGenerated.Identity.
However, your code raises lots of questions.. Why is it a String instead of an Int? This will be a bugbear for using an identity column.
The reason you will want to avoid code like this is because each request will want to query the database to get the "max" ID, as soon as you get two requests running relatively simultaneously you will get 2 requests that say the max ID is "100" before either can reserve and insert 101, so both try to insert 101. By using Identity columns the database will get 2x inserts and give them an ID first-come-first-serve. EF can manage associating FKs around these new IDs automatically for you when you set up navigation properties for the relations. (Rather than trying to set FKs manually which is the typical culprit for developers trying to fetch a new ID app-side)
If you're stuck using an existing schema where the PK is a combination of company ID and this Numero column as a string then about all you can do is implement a retry strategy to account for duplicates:
const int MAXRETRIES = 5;
var session = _httpContextAccessor.HttpContext.User.Claims.ToList();
int currentCompanyId = int.Parse(session[2].Value);
int insertAttemptCount = 0;
while(insertAttempt < MAXRETRIES)
{
try
{
numero = Context.DemandeConge
.Where(t => t.companyID == currentCompanyId)
.Select(p => Convert.ToInt32(p.NumeroDemande))
.DefaultIfEmpty(0)
.Max() + 1;
DemandeConge.NumeroDemande = numero.ToString();
Context.DemandeConge.Add(DemandeConge);
Context.SaveChanges();
break;
}
catch (UpdateException)
{
insertAttemptCount++;
if (insertAttemptCount >= MAXRETRIES)
throw; // Could not insert, throw and handle exception rather than return #null.
}
}
return DemandeConge;
Even this won't be fool proof and can result in failures under load, plus it is a lot of code to work around a poor DB design so my first recommendation would be to fix the schema because coding like this is prone to errors and brittle.
I have a table that consists of a column of pre-populated numbers. My API using Nhibernate grabs the first 10 rows where 'Used' flag is set as false.
What would be the best possible way to avoid concurrency issue when multiple session try to grab row from the table?
After selecting the row, I can update the flag column to be True so subsequent calls will not use the same numbers.
With such a general context, it could be done that way:
// RepeatableRead ensures the read rows does not get concurrently updated by another
// session.
using (var tran = session.BeginTransaction(IsolationLevel.RepeatableRead))
{
var entities = session.Query<Entity>()
.Where(e => !e.Used)
.OrderBy(e => e.Id)
.Take(10)
.ToList();
foreach(var entity in entities)
{
e.Used = true;
}
// If your session flush mode is not the default one and does not cause
// commits to flush the session, add a session.Flush(); call before committing.
tran.Commit();
return entities;
}
It is simple. It may fail with a deadlock, in which case you would have to throw away the session, get a new one, and retry.
Using an optimistic update pattern could be an alternate solution, but this requires some code for recovering from failed attempts too.
Using a no explicit lock solution, which will not cause deadlock risks, could do it, but it will require more queries:
const int entitiesToObtain = 10;
// Could initialize here with null instead, but then, will have to check
// for null after the while too.
var obtainedEntities = new List<Entity>();
while (obtainedEntities.Count == 0)
{
List<Entity> candidates;
using (var tran = session.BeginTransaction())
{
candidatesIds = session.Query<Entity>()
.Where(e => !e.Used)
.Select(e => e.Id)
.OrderBy(id => id)
.Take(entitiesToObtain)
.ToArray();
}
if (candidatesIds.Count == 0)
// No available entities.
break;
using (var tran = session.BeginTransaction())
{
var updatedCount = session.CreateQuery(
#"update Entity e set e.Used = true
where e.Used = false
and e.Id in (:ids)")
.SetParameterList("ids", candidatesIds)
.ExecuteUpdate();
if (updatedCount == candidatesIds.Length)
{
// All good, get them.
obtainedEntities = session.Query<Entity>()
.Where(e => candidatesIds.Contains(e.Id))
.ToList();
tran.Commit();
}
else
{
// Some or all of them were no more available, and there
// are no reliable way to know which ones, so just try again.
tran.Rollback();
}
}
}
This uses NHibernate DML-style operations as suggested here. A strongly typed alternative is available in NHibernate v5.0.
I have the following one to many relationship setup between two tables (Case and Recipient)
Case table has one primary key which is identified as an identity (auto-increment) tblRecipient has one primary key which is identified as an identity (auto-increment),and a foreign key relationship with Case.
Case table and related tblRecipient data is pulled:
function searchCases(caseID, caseNum)
{
var qPredicate;
if (caseID != '')
{
qPredicate = new breeze.Predicate("pkCaseID", "==", parseInt(caseID));
}
else if (caseNum != null)
{
qPredicate = new breeze.Predicate("CaseNumber", "Contains", caseNum);
}
var query = breeze.EntityQuery
.from("case")
.where(qPredicate)
.expand("tblRecipients")
return manager.executeQuery(query);
}
When a button is pressed to add a new recipient, the following code is used to create a new recipient entity:
function createNewRecipient(CaseID)
{
var recipientEntityType = manager.metadataStore.getEntityType("tblRecipient");
var newRecipient = manager.createEntity(recipientEntityType, {fkCaseID: CaseID});
return newRecipient;
}
This code returns this error:
Error: Cannot attach an object to an EntityManager without first setting its key or setting its entityType 'AutoGeneratedKeyType' property to something other than 'None'
The AutoGeneratedKeyType in the metadata shows None instead of Identity as in the Case table. We are not sure what we need to change or how to really debug this. Any help would be appreciated.