i need to calculate some values on the serverside when a specific entity gets updated.
if i update the entity the following code gets executed (C_CompletePrice gets set) and it even gets reflected on clientside (clientside breeze gets all the properties nicely back)
but when i check the db nothing is saved. so when clearing the browser cache and checking the entity again there are the old values...
private bool BeforeSaveTransaction(tblTransactions transaction, EntityInfo info)
{
transaction.C_CompletePrice = 11111111;
return true;
...
protected override bool BeforeSaveEntity(EntityInfo entityInfo)
{
var entity = entityInfo.Entity;
if (entity is tblTransactions)
{
return BeforeSaveTransaction(entity as tblTransactions, entityInfo);
}
...
i'm using breeze 1.4.6
on the server i'm using Breeze.WebApi and Breeze.WebApi.EF
the model i'm using: http://pastebin.com/Dc03DrNe
Update
protected override Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap)
{
foreach (Type entityType in saveMap.Keys)
{
if (entityType.Name == "tblTransactions")
{
foreach (EntityInfo ei in saveMap[entityType])
{
CalculateTransaction(ei);
}
}
}
return base.BeforeSaveEntities(saveMap);
}
private void CalculateTransaction(EntityInfo entityInfo)
{
tblTransactions transaction = (tblTransactions) entityInfo.Entity;
transaction.C_CompletePrice = 1234567;
...
Using BeforeSaveEntities results in the same strange behaviour:
Entites on the client gets updatet :)
DB not :(
So before i'll use now #dominictus solution (overriding SaveAll) i'm kindly asking for the purpose of those methods i've used (bool BeforeSaveEntity(...) and BeforeSaveEntities(saveMap)). I've consulted the doc and i've watched bryan noyes brilliant pluralsight course but still my simple mind doesn't get it :)
Did you update the EntityInfo.OriginalValuesMap as described in the ContextProvider topic?
I do it a bit different. Here is an example where I save a timestamp when object was changed. This is a method in my Context class.
public override int SaveChanges()
{
foreach (
var entry in
this.ChangeTracker.Entries()
.Where((e => (e.State == (EntityState) Breeze.WebApi.EntityState.Added || e.State == (EntityState) Breeze.WebApi.EntityState.Modified))))
{
if (entry.Entity.GetType() == typeof(MyClass))
{
var entity = entry.Entity as MyClass;
if (entity != null) entity.UpdatedDateTime = DateTime.Now;
}
}
return base.SaveChanges();
}
In your case, you could just write: entity.C_CompletePrice = 11111111;
As for method BeforeSaveEntity I prefer using BeforeSaveEntities. Check breeze documentation for examples.
Related
I am struggling to understand how to apply a custom ValueConverter to all the properties of all the models before every insert and update.
According to this resource it can be done:
https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions?tabs=data-annotations
"Value converters allow property values to be converted when reading from or **writing ** to the database"
I was able to accomplish this, but only when reading from the database, overriding ConfigureConventions in the Context:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<string>().HaveConversion<ToUpperConverter>();
}
Converter
public class ToUpperConverter : ValueConverter<string, string>
{
public ToUpperConverter() : base(v => v, v => v.Trim().ToUpper())
{
}
}
Tried also to override the method SaveChangesAsync in the Context, this works but obviously I don't want to go manually for every property and for every model considering they can change at every time.
Using reflection here can be an option, but I am not really a big fan of it.
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
{
foreach (var entry in ChangeTracker.Entries<Player>())
{
if (entry.State == EntityState.Modified || entry.State == EntityState.Added)
{
entry.Entity.Name = entry.Entity.Name.ToUpper();
entry.Entity.MiddleName = entry.Entity.MiddleName?.ToUpper();
entry.Entity.Surname = entry.Entity.Surname.ToUpper();
}
}
return await base.SaveChangesAsync(cancellationToken);
}
Solved, after spending on this an entire day..
The first argument of the converter is the conversion happening to the database, the second argument is the conversion happening from the database.
public class ToUpperConverter : ValueConverter<string, string>
{
public ToUpperConverter() : base(
// writing to the database
v => v.Trim().ToUpper(),
// reading from the database
v => v.Trim().ToUpper())
{
}
}
Source: https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.builders.propertiesconfigurationbuilder.haveconversion?view=efcore-6.0
"The type to convert to and from or a type that inherits from ValueConverter."
Is there any way to get an Entity's navigational property's "current" value in BeforeSaveEntity (or anywhere else before save) in breeze on the server side? By current, I mean what exists in the database, with any incoming changes merged in. This isn't for validation, rather I am computing a value for a parent property (that I don't want on the client) based upon both the parent fields and children fields...
For example,
public class Parent {
public ICollection<Child> Children{ get; set; }
}
. . .
protected override bool BeforeSaveEntity(EntityInfo entityInfo) {
if (entityInfo.Entity.GetType() == typeof(Parent) &&
(entityInfo.EntityState == EntityState.Added || entityInfo.EntityState == EntityState.Updated)) {
// Lazy load Parent's Children collection out of breeze's context
// so items are "current' (existing merged with changes)
Parent parent = (Parent)entityInfo.Entity;
Context.Entry(parent).Collection(p => p.Children).Load();
// this throws exception Member 'Load' cannot be called for property
// 'Children' because the entity of type 'Parent' does not exist in the context.
}
}
I'm thinking they are not in the DBContext yet. All I can think to do is to retrieve the existing children from the database, and hand merge the changes in BeforeSaveEntities, which is a hassle.
Lazy loading is not enable in the DbContext that Breeze uses for saving. The reason is detailed in this SO answer.
You should load any additional entities in a separate DbContext.
Here's an example of how I did it in a project. Perhaps the MergeEntities and DetachEntities methods should be included with Breeze to make it simpler to do this.
protected override Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap)
{
// create a separate context for computation, so we don't pollute the main saving context
using (var newContext = new MyDbContext(EntityConnection, false))
{
var parentFromClient = (Parent)saveMap[typeof(Parent)][0].Entity;
// Load the necessary data into the newContext
var parentFromDb = newContext.Parents.Where(p => p.ParentId == parentFromClient.ParentId)
.Include("Children").ToList();
// ... load whatever else you need...
// Attach the client entities to the ObjectContext, which merges them and reconnects the navigation properties
var objectContext = ((IObjectContextAdapter)newContext).ObjectContext;
var objectStateEntries = MergeEntities(objectContext, saveMap);
// ... perform your business logic...
// Remove the entities from the second context, so they can be saved in the original context
DetachEntities(objectContext, saveMap);
}
return saveMap;
}
/// Attach the client entities to the ObjectContext, which merges them and reconnects the navigation properties
Dictionary<ObjectStateEntry, EntityInfo> MergeEntities(ObjectContext oc, Dictionary<Type, List<EntityInfo>> saveMap)
{
var oseEntityInfo = new Dictionary<ObjectStateEntry, EntityInfo>();
foreach (var type in saveMap.Keys)
{
var entitySet = this.GetEntitySetName(type);
foreach(var entityInfo in saveMap[type])
{
var entityKey = oc.CreateEntityKey(entitySet, entityInfo.Entity);
ObjectStateEntry ose;
if (oc.ObjectStateManager.TryGetObjectStateEntry(entityKey, out ose))
{
if (ose.State != System.Data.Entity.EntityState.Deleted)
ose.ApplyCurrentValues(entityInfo.Entity);
}
else
{
oc.AttachTo(entitySet, entityInfo.Entity);
ose = oc.ObjectStateManager.GetObjectStateEntry(entityKey);
}
if (entityInfo.EntityState == Breeze.ContextProvider.EntityState.Deleted)
{
ose.Delete();
}
oseEntityInfo.Add(ose, entityInfo);
}
}
return oseEntityInfo;
}
/// Remove the entities in saveMap from the ObjectContext; this separates their navigation properties
static void DetachEntities(ObjectContext oc, Dictionary<Type, List<EntityInfo>> saveMap)
{
foreach (var type in saveMap.Keys)
{
foreach (var entityInfo in saveMap[type])
{
try
{
oc.Detach(entityInfo.Entity);
}
catch
{ // the object cannot be detached because it is not attached
}
}
}
}
Is the essence of Project, the creation of which is necessary to check whether there is already an entity with the same name. When editing needs such as checking, but keep in mind that the old and the new name of the entity can be matched.
You also need to display an error message. For this I use interface IValidatableObject, but do not know how to tell the Validate method the object is currently being edited or created
DbContext.ValidateEntity takes the IDictionary<Object, Object> items as the second parameter. You can pass any data there and the data you pass will be passed to IValidatableObject.Validate in the ValidationContext.Items
Assuming you refer to check EF cant do for you.
This is actually difficult to check. You are checking an entity after it has been added to the context. It should not check itself and needs to consider other items in context that are not yet saved. As well as the DB. There are several 3 combinations plus an self recognition. Record a an entity record in LOCAL when ID is blank/new ie multiple new inserts needs careful coding. (Consider using temp IDs)
the not yet saved entries should be in context
Context.Set<TPoco>().Local
and get data from DB and keep in a temp list. BUT dont put in context.
Or use a SECOND context.
var matchingSet = Context.Set<TPoco>().AsNoTracking() // not into context...
.Where(t=>t.field == somevalue).ToList();
So what about logical and actual duplicates on the DB. Logical duplicates are duplicates on a field with no unique index that from a business perspective should be unique.
If you want to check those...
You need to read the DB.... BUT if these records are currently being changed, you CAN NOT just put them into the Context. You would overwrite them.
But what if the values the logical key values have changed?
Something caused a logical dup on a record on the DB may no longer be a dup once saved or vice verse. Is that still a dup or not ?
So you need to decide how you match LOCAL versus loaded records.
Ie check LOCAL and matching DB records and decidr what to do if a record is in both, only local or only db.
LOCAL ONLY and DB Only is easy.
But in both... That is your business process decision.
Problem is solved using method ModelState.AddModelError (string, string) in actions Edit and Create.
[HttpPost]
[HandleError(View="AjaxError")]
public ActionResult Edit(ProjectsViewData data)
{
if (ModelState.IsValid)
{
if (!ContainsProject(data.CurrentObject.Name))
{
db.Projects.Attach(data.CurrentObject);
db.ObjectStateManager.ChangeObjectState(data.CurrentObject, EntityState.Modified);
db.SaveChanges();
return Projects(data);
}
else
{
int projectId = (from p in db.Projects
where p.Name == data.CurrentObject.Name
select p.ProjectID).FirstOrDefault();
if (projectId == data.CurrentObject.ProjectID)
{
db.Projects.Attach(data.CurrentObject);
db.ObjectStateManager.ChangeObjectState(data.CurrentObject, EntityState.Modified);
db.SaveChanges();
return Projects(data);
}
else
{
ModelState.AddModelError("Name", Localizer.ProjectAlreadyExists);
}
}
}
data.ObjectToEdit = data.CurrentObject;
return Projects(data);
}
[HttpPost]
[HandleError(View = "AjaxError")]
public ActionResult Create(ProjectsViewData data)
{
if (ModelState.IsValid)
{
if (!ContainsProject(data.CurrentObject.Name))
{
db.Projects.AddObject(data.CurrentObject);
db.SaveChanges();
return Projects(data);
}
else
{
ModelState.AddModelError("Name", Localizer.ProjectAlreadyExists);
}
}
data.ObjectToAdd = data.CurrentObject;
return Projects(data);
}
Helper method:
private bool ContainsProject(string projectName)
{
if (projectName != null)
{
projectName = Regex.Replace(projectName.Trim(), "\\s+", " ");
List<string> projects = new List<string>();
var projectNames = (from p in db.Projects
select p.Name.Trim()).ToList();
foreach (string p in projectNames)
{
projects.Add(Regex.Replace(p, "\\s+", " "));
}
if (projects.Contains(projectName))
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
I have DataContext.Refresh Method:
public void RefreshDataSource()
{
_entities.Refresh(RefreshMode.ClientWins,Departments);
}
And observable collection:
public ObservableCollection<Department> Departments
{
get
{
if (_departments == null && _entities != null)
{
_entities.Departments.Include("Drivers").ToArray();
_departments = new EntityObservableCollection<Department>(_entities.Departments);
}
return _departments;
}
}
If i update records outside context i see only changed records but can't see inserted and removed. Why?
Because Refresh doesn't look for new records. It takes records you already have and updates them with current values. It also probably doesn't handle deleted records especially if you use ClientWins strategy which takes your state as more important.
In EF code first, one specifies field properties and relationships using the fluent interface. This builds up a model. Is it possible to get a reference to this model, and reflect on it?
I want to be able to retrieve for a given field, if it is required, what its datatype is, what length, etc...
You need to access the MetadataWorkspace. The API is pretty cryptic. You may want to replace DataSpace.CSpace with DataSpace.SSpace to get the database metadata.
public class MyContext : DbContext
{
public void Test()
{
var objectContext = ((IObjectContextAdapter)this).ObjectContext;
var mdw = objectContext.MetadataWorkspace;
var items = mdw.GetItems<EntityType>(DataSpace.CSpace);
foreach (var i in items)
{
foreach (var member in i.Members)
{
var prop = member as EdmProperty;
if (prop != null)
{
}
}
}
}