Hi!
I haven't tried anything like this myself, but here is a guess: The DTO doesn't necessarily represent everything that is in the database. Most probably a DTO was loaded from the database, copied, modified and sent to the save method which raises the event you are listening to. But which tables' data are included in the DTO depends on the ResponseGroup used when it was loaded. But then you say that it is always null, and I'm pretty sure when something is edited from the UI it will use a response group that includes variations, so this might not be it at all. Have you checked if the Variation table contains anything at all?
The Variation table has all the rows that was expected. Im trying this out with a Catalog.zip containing a mix of products and variations and all 182 variations is on the first run successfully added to the Variation table. However when I try to run the import again it fails since the check for existing VariationRow fails and the code for inserting a VariationRow is used when I would like either nothing happening or at least some kind of update of the VariationRow to happen
4/12/2016 8:15:38 AM: Violation of PRIMARY KEY constraint 'PK_ProductVariation'. Cannot insert duplicate key in object 'dbo.Variation'. The duplicate key value is (1265). The statement has been terminated. at System.Data.Common.DbDataAdapter.UpdatedRowStatusErrors(RowUpdatedEventArgs rowUpdatedEvent, BatchCommandInfo[] batchCommands, Int32 commandCount) at System.Data.Common.DbDataAdapter.UpdatedRowStatus(RowUpdatedEventArgs rowUpdatedEvent, BatchCommandInfo[] batchCommands, Int32 commandCount) at System.Data.Common.DbDataAdapter.Update(DataRow[] dataRows, DataTableMapping tableMapping) at System.Data.Common.DbDataAdapter.Update(DataRow[] dataRows) at EPiServer.Data.Providers.SqlTransientErrorsRetryPolicy.Execute[TResult](Func`1 method) at Mediachase.Data.Provider.SqlDataProvider.SaveRows(DataCommand command) at Mediachase.Commerce.Storage.DataHelper.SaveTableSimpleWorker(DataCommand cmd, DataTable table, DataViewRowState state) at Mediachase.Commerce.Storage.DataHelper.SaveDataSetSimple(DataCommand cmd, DataSet set, String[] tables) at Mediachase.Commerce.Catalog.Data.CatalogEntryAdmin.Save() at Mediachase.Commerce.Catalog.Managers.CatalogEntryManager.SaveCatalogEntry(CatalogEntryDto dataset) at Mediachase.Commerce.Catalog.ImportExport.CatalogImportExport.SaveEntryDto(CatalogEntryDto workingCatalogEntryDto, Dictionary`2 metaObjectsList, Dictionary`2 priceGroups, Dictionary`2 warehouseInventories) at Mediachase.Commerce.Catalog.ImportExport.CatalogImportExport.ReadEntries(Guid applicationId, Int32 catalogId, XmlReader reader, String baseFilePath, Int32 totalCount, String defaultCurrency, Boolean overwrite, IEnumerable`1 catalogLanguages) at Mediachase.Commerce.Catalog.ImportExport.CatalogImportExport.Import(Stream input, Guid applicationId, String baseFilePath, Boolean overwrite) at Mediachase.Commerce.Catalog.ImportExport.ImportJob.<>c__DisplayClass7.<DoImport>b__5() at Mediachase.Commerce.Catalog.ImportExport.ImportJob.WithApplicationId(Guid applicationId, Action action) at Mediachase.Commerce.Catalog.ImportExport.ImportJob.DoImport(String[] stages, Int32 stageIndex, String sourceFile, String sourceDirectory) at Mediachase.Commerce.Catalog.ImportExport.ImportJob.Execute(Action`1 addMessage, CancellationToken cancellationToken) at Mediachase.Commerce.BackgroundTasks.BackgroundTaskState.Execute(CancellationToken cancellationToken)
4/12/2016 8:15:38 AM: Task failed with exception.
4/12/2016 8:15:03 AM: Warning - Overwriting Entry with code '22017134'.
So do I need to load something in a different way to get the correct information?
Solved it by getting the entryDto after clearing the cache and with ResponseGroup.CatalogEntryFull:
CatalogEntryDto.CatalogEntryRow cleanRow = null; string cacheKey = CatalogCache.CreateCacheKey("catalogentry", responseGroup.CacheKey, catalogEntryRow.CatalogEntryId.ToString()); CatalogCache.Remove(cacheKey); CatalogEntryDto entryDto = CatalogContext.Current.GetCatalogEntryDto(catalogEntryRow.CatalogEntryId, responseGroup); if (entryDto != null) { cleanRow = entryDto.CatalogEntry.FirstOrDefault(); } var variationRow = cleanRow.GetVariationRows().FirstOrDefault();
I don't think you have to clear the cache. It should work correctly with CatalogEntryFull only - if you have to remove the cache then it's a problem we have to look into
Regards,
/Q
Ok yeah the cache clearance was not necessary. But can someone please assist me in the final touches to get this working properly.
Im having problem getting the information to save into the Variation table
If I put the code in the EntryUpdating part as in the code above it won't work when there's new entrys to be written. I didn't see this at first since I hade the entries I tried with already in the database. What happens then is that the catalogEntryRow.CatalogEntryId is neagtive because it has not yet been saved to the database. However if the code is kept in the EntryUpdating the correct information will be saved if all items already exists in the database. I believe that might have to do with the entry being saved later on and all changes are then saved.
However putting it in the EntryUpdated solves the inventory setting and the code for variation row works but nothing ends up in the Variation table. How do I save it correctly?
if (variationRow == null) { CatalogEntryDto.VariationRow newVariationRow = entry.Variation.NewVariationRow(); newVariationRow.ListPrice = Convert.ToDecimal(0); newVariationRow.MaxQuantity = 10000; newVariationRow.SetMerchantIdNull(); newVariationRow.MinQuantity = 0; newVariationRow.PackageId = 0; newVariationRow.TaxCategoryId = 0; newVariationRow.TrackInventory = false; newVariationRow.WarehouseId = 0; newVariationRow.Weight = Convert.ToDouble(1); newVariationRow.CatalogEntryId = catalogEntryRow.CatalogEntryId; if (newVariationRow.RowState == DataRowState.Detached) entry.Variation.AddVariationRow(newVariationRow); }
Also I'm a bit curious into why I must do the
foreach (var catalogEntryRow in entry.CatalogEntry) {
Because when I started out trying to get this to work I followed jondjones example of this at http://jondjones.com/how-to-hook-into-episerver-commerces-8-catalog-event-handlers/ and I thought the the EntryUpdated would be triggered for each and every entry. But it seems to come in batches because when I tried with the import file which contains 44 nodes and 343 entries it was just triggered twice.
Adding the rows referencing the negative ids should work too, I don't know why it doesn't...
In an EntryUpdated handler the DTO is already saved and won't be saved again, so you have to save yourself. The suggested approach is to always copy a DTO before modifying and saving it:
var entry = (CatalogEntryDto)source;
entry = (CatalogEntryDto)entry.Copy();
// do your updates
CatalogContext.Current.SaveCatalogEntry(entry); // Note: Beware of recursion as this will trigger your event handler again!
The reason it behaves differently from that example is that from Commerce 9 entries in the import are saved in batches (a performance optimization).
The problem with the negative ids is on this row:
CatalogEntryDto entryDto = CatalogContext.Current.GetCatalogEntryDto(catalogEntryRow.CatalogEntryId, responseGroup);
It doesn't return an entryDto when the database is empty. So thats stopping med from having the code in EntryUpdating.
But your final comments seems to have corrected the VariationRow saving and it works. Here is the final code:
using EPiServer.ServiceLocation; using Mediachase.Commerce.Catalog; using Mediachase.Commerce.Catalog.Dto; using Mediachase.Commerce.Catalog.Events; using Mediachase.Commerce.Catalog.Managers; using Mediachase.Commerce.Inventory; using Mediachase.Commerce.InventoryService; using System; using System.Data; using System.Linq; namespace EPiServer.Reference.Commerce.Manager.Infrastructure.Indexing { [ServiceConfiguration(typeof(CatalogEventListenerBase))] public class CommerceIndexingModule : CatalogEventListenerBase { private Injected<IInventoryService> _inventoryService; private Injected<IWarehouseRepository> _warehouseService; public override void EntryUpdated(object source, EntryEventArgs args) { base.EntryUpdated(source, args); var entry = (CatalogEntryDto)source; entry = (CatalogEntryDto)entry.Copy(); var responseGroup = new CatalogEntryResponseGroup(CatalogEntryResponseGroup.ResponseGroup.CatalogEntryFull); foreach (var catalogEntryRow in entry.CatalogEntry) { if (catalogEntryRow == null) continue; if (catalogEntryRow.ClassTypeId == "Variation") { var inventoryRecords = _inventoryService.Service.QueryByEntry(new[] { catalogEntryRow.Code }); if (inventoryRecords.Count > 0) { var inventoryRecord = inventoryRecords.FirstOrDefault(); if (inventoryRecord.PurchaseAvailableQuantity != 100000) { inventoryRecord.PurchaseAvailableQuantity = 100000; _inventoryService.Service.Update(new[] { inventoryRecord }); } } else { _inventoryService.Service.Insert(new[] { new InventoryRecord { AdditionalQuantity = 0, BackorderAvailableQuantity = 0, BackorderAvailableUtc = DateTime.UtcNow, CatalogEntryCode = catalogEntryRow.Code, IsTracked = true, PreorderAvailableQuantity = 0, PreorderAvailableUtc = DateTime.UtcNow, PurchaseAvailableQuantity = 100000, PurchaseAvailableUtc = DateTime.UtcNow, WarehouseCode = _warehouseService.Service.GetDefaultWarehouse().Code } }); } CatalogEntryDto.CatalogEntryRow cleanRow = null; CatalogEntryDto entryDto = CatalogContext.Current.GetCatalogEntryDto(catalogEntryRow.CatalogEntryId, responseGroup); if (entryDto == null) continue; cleanRow = entryDto.CatalogEntry.FirstOrDefault(); if (cleanRow == null) continue; var variationRow = cleanRow.GetVariationRows().FirstOrDefault(); if (variationRow == null) { CatalogEntryDto.VariationRow newVariationRow = entry.Variation.NewVariationRow(); newVariationRow.ListPrice = Convert.ToDecimal(0); newVariationRow.MaxQuantity = 10000; newVariationRow.SetMerchantIdNull(); newVariationRow.MinQuantity = 0; newVariationRow.PackageId = 0; newVariationRow.TaxCategoryId = 0; newVariationRow.TrackInventory = false; newVariationRow.WarehouseId = 0; newVariationRow.Weight = Convert.ToDouble(1); newVariationRow.CatalogEntryId = catalogEntryRow.CatalogEntryId; if (newVariationRow.RowState == DataRowState.Detached) entry.Variation.AddVariationRow(newVariationRow); } } } CatalogContext.Current.SaveCatalogEntry(entry); } } }
Ah, I thought you were back to the first approach where you didn't fetch an extra DTO. Fetching from DB with the negative ID will not work.
It still troubles me a bit that you can't get it working without re-fetching and saving a second dto. The code in your first example looks like it would work, though the absense of a variation row doesn't mean there is none in the database (the responsegroup thing). But I'm still surprised you never saw anything in the table.
What you could probably do though, if you care to investigate the EntryUpdating approach more, is to check if the CatalogEntryRow you currently have in the loop has rowstate added. If so, and there is no variation row corresponding to it, it should mean there is no variation row in db and you can create one. If the entry row is instead in state modified and you can't find a variation row in the table it either means there is none in the db or the DTO wasn't loaded with that response group. In this case you should have the ID and you can fetch a separate DTO. For rows with state deleted you should of course not try to add any variation rows.
I will check the EntryUpdating a bit more but I can't get the inventory update to work in EntryUpdating. Getting
An exception of type 'System.Data.SqlClient.SqlException' occurred in EPiServer.Data.dll but was not handled in user code
Additional information: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_ManagedInventory_CatalogEntry"
Im guessing the entry has not yet benn saved and therefor I can't connect inventory to it.
I think you can make that code run after the entry dto is saved by adding a TransactionScope.OnCommit handler:
TransactionScope.OnCommit(() =>
{
// Code here
});
Beware though that when that runs the DTO will have been saved so the state of it may be different. If you want to act exactly on the state as it is in the EntryUpdating handler you should extract any you need from the DTO into other variables and and use those in the oncommit action to make sure you capture the expected data for the closure.
Ok got the EntryUpdating path working using this code. I also came into checking if the row exists and then try to update it. But the following code does not correctly save a new value in the variation row. How does that work? If I look at the rowstate of the variationRow before AcceptChanges it is Modified and after AcceptChanges Unchanged but no updated value has been aded to the database.
public override void EntryUpdating(object source, EntryEventArgs args) { base.EntryUpdating(source, args); var entry = (CatalogEntryDto)source; //entry = (CatalogEntryDto)entry.Copy(); var responseGroup = new CatalogEntryResponseGroup(CatalogEntryResponseGroup.ResponseGroup.CatalogEntryFull); foreach (var catalogEntryRow in entry.CatalogEntry) { if (catalogEntryRow == null) continue; if (catalogEntryRow.ClassTypeId == "Variation") { if (catalogEntryRow.RowState == DataRowState.Added) { CatalogEntryDto.VariationRow newVariationRow = entry.Variation.NewVariationRow(); newVariationRow.ListPrice = Convert.ToDecimal(0); newVariationRow.MaxQuantity = 10000; newVariationRow.SetMerchantIdNull(); newVariationRow.MinQuantity = 0; newVariationRow.PackageId = 0; newVariationRow.TaxCategoryId = 0; newVariationRow.TrackInventory = false; newVariationRow.WarehouseId = 0; newVariationRow.Weight = Convert.ToDouble(1); newVariationRow.CatalogEntryId = catalogEntryRow.CatalogEntryId; if (newVariationRow.RowState == DataRowState.Detached) entry.Variation.AddVariationRow(newVariationRow); } else if (catalogEntryRow.RowState == DataRowState.Modified || catalogEntryRow.RowState == DataRowState.Unchanged) { CatalogEntryDto.CatalogEntryRow cleanRow = null; CatalogEntryDto entryDto = CatalogContext.Current.GetCatalogEntryDto(catalogEntryRow.CatalogEntryId, responseGroup); if (entryDto == null) continue; cleanRow = entryDto.CatalogEntry.FirstOrDefault(); if (cleanRow == null) continue; var variationRow = cleanRow.GetVariationRows().FirstOrDefault(); if (variationRow == null) { CatalogEntryDto.VariationRow newVariationRow = entry.Variation.NewVariationRow(); newVariationRow.ListPrice = Convert.ToDecimal(0); newVariationRow.MaxQuantity = 10000; newVariationRow.SetMerchantIdNull(); newVariationRow.MinQuantity = 0; newVariationRow.PackageId = 0; newVariationRow.TaxCategoryId = 0; newVariationRow.TrackInventory = false; newVariationRow.WarehouseId = 0; newVariationRow.Weight = Convert.ToDouble(1); newVariationRow.CatalogEntryId = catalogEntryRow.CatalogEntryId; if (newVariationRow.RowState == DataRowState.Detached) entry.Variation.AddVariationRow(newVariationRow); } else { variationRow.MaxQuantity = 20000; variationRow.AcceptChanges(); } } } } }
You should not call AcceptChanges. As you noticed that resets the row state so it will be skipped when the DTO is saved. However since that row is coming from a separate DTO you need to import it into the dto chat is going to be saved after the event completes:
entry.Variation.ImportRow(variationRow)
Why not just simply hook up with published event to achieve this? I have done this recently by using published event, and everything works like a charm.
I haven't thought much about what you are trying to achieve, just advising on what to do to make the code work :)
Anyway, adding to my last post, you should preferrably import the row before modifying it, otherwise you are modifying on a potentially shared resource since the DTOs are loaded through cache.
This is running as part of the manager code in a Quicksilver based solution to set this information since inRiver doesn't set it when publishing information to Episerver and the basic workflows for the cart don't work without them. But is there a better way to do this I would gladly look into it. This approach was suggested to me from inRiver and after all the initial struggles it feels like a rather effective way to achive it with regards to the batch based approach of the importer. But maybe there's better ways.
Yeah the syntax of the DTOs and their lifecycles is a lot to get your head around and require you to get your hands dirty and produce rather verbose code but when you begin to understand all the idiosyncrasies of it it works pretty well and the batching capabilities are very powerful compared to the content model.
Hi Tobias
Below is what I recently implemented in my solution (I am bit lazy to go to Commerce manager every time when I need to add inventory for a test product. :)) I hope this can help you solve your problem.
public void PublishedContent(object sender, ContentEventArgs e) { #if DEBUG var entry = this.LoadEntry(); var firstWarehouse = EPiServer.ServiceLocation.ServiceLocator.Current .GetInstance<Mediachase.Commerce.Inventory.IWarehouseRepository>() .List().FirstOrDefault(); var warehouseInventoryService = EPiServer.ServiceLocation.ServiceLocator.Current.GetInstance<Mediachase.Commerce.Inventory.IWarehouseInventoryService>(); var currentInventory = warehouseInventoryService.Get(new CatalogKey(entry), firstWarehouse); if(currentInventory == null) { var inventoryService = EPiServer.ServiceLocation.ServiceLocator.Current.GetInstance<Mediachase.Commerce.InventoryService.IInventoryService>(); inventoryService.Insert(new[] { new Mediachase.Commerce.InventoryService.InventoryRecord { AdditionalQuantity = 0, BackorderAvailableQuantity = 0, BackorderAvailableUtc = DateTime.UtcNow, CatalogEntryCode = new CatalogKey(entry).CatalogEntryCode, IsTracked = true, PreorderAvailableQuantity = 0, PreorderAvailableUtc = DateTime.UtcNow, PurchaseAvailableQuantity = 1000, PurchaseAvailableUtc = DateTime.UtcNow, WarehouseCode = firstWarehouse.Code } }); } else { if (currentInventory.InStockQuantity < 100) { var inventory = new Mediachase.Commerce.Inventory.WarehouseInventory() { CatalogKey = new CatalogKey(entry), WarehouseCode = firstWarehouse.Code, InStockQuantity = 1000, InventoryStatus = Mediachase.Commerce.Inventory.InventoryTrackingStatus.Enabled }; warehouseInventoryService.Save(inventory); } } #endif }
Thanks for the input, works like a charm.
Is there a simple way to get hold of the properties of the specific entry without causing to much overhead? When the information is synced from inRiver we get a metafield called ItemWeight. What I would like to do is to put that value into the Weight property of the VariationRow. But can those values be read even if the entry has not been saved to the database? If this can not be done without causing to much overhead we will solve it in another way, more related to the cart/checkout.
I running the following CommerceEventListener:
This sets or updates the inventory and variationrow for each variation so that the workflows connected to the cart will validate. It sets the information correctly and the inventory is updated correctly but I'm having issues with the
This always returns null even if the row exists in the Variation table. How to correctly check for existing VariationRow and if needed update the min/max quantity of it?