Importing Thousands of Records Into Acumatica via SOAP Contract-Based API - acumatica

I’m using contract-based SOAP APIs to try to import about 25,000 Journal Entry lines from a banking system into a single Acumatica GL batch.
If I try to add all the records at once to the same GL batch, my
request times out after a few hours. Since it uses the same GL
batch, this solution does not leverage multi-threading.
I've also tried adding the 25000 lines one line at a time to a
single GL batch and the requests does not time out, but
performance-speed starts decreasing significantly after
approximately 3000 records or so are added to the GL batch. This
process takes several hours to run and since it uses the same GL
batch, this solution does not leverage multi-threading.
I looked into multi-threading as well to import the data into
several smaller GL-batches of 5000 lines each and that works without
any timeout issues. but it still takes about an hour and a half to
run. Also, the customer does not accept this multi-batch approach;
they want all their daily data in a single GL batch.
25,000 records does not seem like a lot to me, so I wonder if Acumatica’s APIs were not built for this volume of lines in a single transaction. All I’m doing in my code is building the entity info by reading a text file and then calling the put method to create the GL batch using that entity with 25,000 line records.
I've read a couple of articles about optimizing the APIs, but they primarily deal with different instances of an entity, as in several different GL batches or several different Stock Items for example. In those cases, multi-threading is a great asset because you can have multiple threads creating multiple "different" GL batches, but multi-threading is not helpful when updating the same GL batch.
Here's what I've read so far:
https://asiablog.acumatica.com/2016/12/optimizing-large-import.html
https://adn.acumatica.com/blog/contractapioptimization/
I'm at a loss here, so any pointers would be greatly appreciated.
I look forward to your response.
Here's my code:
public static void CreateMultipleLinesPerJournalEntryBatchContractTEST(MyStoreContract.DefaultSoapClient soapClient, List<JournalEntry> journalEntries)
{
string myModuleForBatchLookup = "GL";
//list holding the values of all the records belonging to the batch in process
List<JournalEntry> allBatchItems = journalEntries;
//List used to store objects in format required by Acumatica
List<MyStoreContract.JournalTransactionDetail> myJournalTransactionsFormatted = new List<MyStoreContract.JournalTransactionDetail>();
try
{
//Creating a header and returning a batch value to be used for all line iterations.
JournalEntry myHeaderJournalEntryContract = allBatchItems.First();
string myBatchNumberToProcess = AddGLBatchHeaderContractTEST(soapClient, myHeaderJournalEntryContract);
// Do something with then n number of items defined in processing subBatch size or remaining items if smaller
foreach (JournalEntry je in allBatchItems)
{
//Moving the items in each batch from the original unformatted list to the formatted list one at a time
myJournalTransactionsFormatted.Add(new MyStoreContract.JournalTransactionDetail
{
BranchID = new MyStoreContract.StringValue { Value = je.Branch },
Account = new MyStoreContract.StringValue { Value = je.Account },
Subaccount = new MyStoreContract.StringValue { Value = je.Subaccount },
ReferenceNbr = new MyStoreContract.StringValue { Value = je.RefNumber },
DebitAmount = new MyStoreContract.DecimalValue { Value = je.DebitAmount },
CreditAmount = new MyStoreContract.DecimalValue { Value = je.CreditAmount },
TransactionDescription = new MyStoreContract.StringValue { Value = je.TransactionDescription },
UsrTransactionTime = new MyStoreContract.StringValue { Value = je.UsrTransactionTime },
UsrTransactionType = new MyStoreContract.StringValue { Value = je.UsrTransactionType },
UsrTranSequence = new MyStoreContract.StringValue { Value = je.UsrTranSequence },
UsrTellerID = new MyStoreContract.StringValue { Value = je.UsrTellerID }
});
}
//Specify the values of a new Jornal Entry using all the collected elements from the batch(list) created
MyStoreContract.JournalTransaction journalToBeCreated = new MyStoreContract.JournalTransaction
{
//Header data and details added by list generated by loop
BatchNbr = new MyStoreContract.StringSearch { Value = myBatchNumberToProcess }, //This is one of two lines used to lookup/search the batch needing to be updated
Module = new MyStoreContract.StringSearch { Value = myModuleForBatchLookup }, //This is one of two lines used to lookup/search the batch needing to be updated
Details = myJournalTransactionsFormatted.ToArray() // this is the line adding the array containing all the line details
};
soapClient.Put(journalToBeCreated);
Console.WriteLine("Added " + allBatchItems.Count.ToString() + " line transactions");
Console.WriteLine();
Console.WriteLine("Press any key to continue");
Console.ReadLine();
}
catch (Exception e)
{
Console.WriteLine("The following error was encountered and all entries for this batch need to be logged in error table");
Console.WriteLine();
Console.WriteLine(e.Message);
Console.WriteLine();
Console.WriteLine("Press any key to continue");
Console.ReadLine();
}
}
public static string AddGLBatchHeaderContractTEST(MyStoreContract.DefaultSoapClient soapClient, JournalEntry je)
{
try
{
//Specify the values of a new Jornal Entry Batch header
MyStoreContract.JournalTransaction journalToBeCreated = new MyStoreContract.JournalTransaction
{
//Header data
BranchID = new MyStoreContract.StringValue { Value = "PRODWHOLE" }, //This is the default branch
TransactionDate = new MyStoreContract.DateTimeValue { Value = je.TransactionDate.AddDays(-1) }, //Reduced 1 day from the batch
CurrencyID = new MyStoreContract.StringValue { Value = je.CurrencyCode }, //Currency to be used for the batch
Description = new MyStoreContract.StringValue { Value = je.TransactionDescription },
Hold = new MyStoreContract.BooleanValue { Value = true }
};
//Create a Journal Entry with the specified values
MyStoreContract.JournalTransaction newJournalTransaction = (MyStoreContract.JournalTransaction)soapClient.Put(journalToBeCreated);
string myBatchToProcess = newJournalTransaction.BatchNbr.Value;
return myBatchToProcess;
}
catch (Exception e)
{
Console.WriteLine("Error was caught while trying to create the header for the batch...");
Console.WriteLine();
Console.WriteLine(e);
Console.WriteLine();
return null;
}
}
My custom class for legacy system line items which I then need format into Acumatica's format:
class JournalEntry
{
public DateTime TransactionDate { get; set; }
public string CurrencyCode { get; set; }
public string Description { get; set; }
public string Branch { get; set; }
public string Account { get; set; }
public string Subaccount { get; set; }
public string RefNumber { get; set; }
public decimal DebitAmount { get; set; }
public decimal CreditAmount { get; set; }
public string TransactionDescription { get; set; }
//Added custom fields for customer
public string UsrTellerID { get; set; }
public string UsrTransactionType { get; set; }
public string UsrTransactionTime { get; set; }
public string UsrTranSequence { get; set; }
//Adding original file data for the line
public string FileLineData { get; set; }
}
I tried Yuriy's approach described below, but my custom fields are not updating. Only standard fields are being updated. Which command should I use to update the extension (custom) fields. See code below:
//Here I create instance of GLTran
GLTran row = graph.GLTranModuleBatNbr.Cache.CreateInstance() as GLTran;
//here I get a handle to graph extension GLTranExt to be able to use the added fields.
var rowExt = row.GetExtension<GLTranExt>();
row = graph.GLTranModuleBatNbr.Insert(row);
graph.GLTranModuleBatNbr.Cache.SetValueExt(row, "AccountID", JE.Account);
graph.GLTranModuleBatNbr.Cache.SetValueExt(row, "SubID", JE.Subaccount);
row.TranDesc = "my line description";
row.Qty = 1.0m;
row.CuryDebitAmt = (JE.DebitAmount);
row.CuryCreditAmt = (JE.CreditAmount);
rowExt.UsrTellerID = "Test teller";
rowExt.UsrTransactionTime = "Test Transaction Time";
rowExt.UsrTransactionType = "Test Transaction Type";
rowExt.UsrTranSequence = "Test Transaction Sequence";
row = graph.GLTranModuleBatNbr.Update(row);
graph.Actions.PressSave();

In multi threaded import of Sales Orders I've got 18000 lines per hour ( 4 cores, 32Gb RAM ). So your 25000 is very similar to what I've get ( one Sales order had 1 - 6 lines ). For second link that you provided, what were parameters of your API call, what was number of your Acumatica instances ( CPU, RAM, parameters of SQL Server )?
I propose you to consider scaling Acumatica horizontally and also scale your database via SQL sharding.
Edit
In case if you need to have one GL Batch with 25000 lines on it, then I propose you following workaround:
Create one more Acumatica page that has text box and button Import.
In code of button Import button
2.1 read text box information as xml ( or JSON )
2.2 Create instance of GL Graph
2.3 Insert via Graph needed amount ( in your case 25000 ) lines
2.4 Call to graph.PressSave()
Send Web API your request not to GL Batch but to created by you page.

I know this is an old question, but I am answering here for the benefit of anyone who stumbles across this page. There's a performance issue in the Journal Transaction screen where the time to create a transaction increases non-linearly with the number of rows to insert.
A customization-based workaround was provided to us by Acumatica support which significantly improved performance. I don't have the exact version where this fix was included, but builds newer than today (September 2021) should include this already.
Customization fix:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using PX.Api;
using PX.Data;
using PX.Common;
using PX.Objects.Common;
using PX.Objects.Common.Extensions;
using PX.Objects.CS;
using PX.Objects.CM;
using PX.Objects.CA;
using PX.Objects.Common.Bql;
using PX.Objects.Common.GraphExtensions.Abstract;
using PX.Objects.Common.GraphExtensions.Abstract.DAC;
using PX.Objects.Common.GraphExtensions.Abstract.Mapping;
using PX.Objects.GL.DAC;
using PX.Objects.GL.FinPeriods;
using PX.Objects.GL.JournalEntryState;
using PX.Objects.GL.JournalEntryState.PartiallyEditable;
using PX.Objects.GL.Overrides.PostGraph;
using PX.Objects.GL.Reclassification.UI;
using PX.Objects.PM;
using PX.Objects.TX;
using PX.Objects.Common.Tools;
using PX.Objects.GL.DAC.Abstract;
using PX.Objects.Common.EntityInUse;
using PX.Objects.GL.FinPeriods.TableDefinition;
using PX.Data.SQLTree;
using PX.Objects.CR;
using PX.Data.BQL.Fluent;
using PX.Data.BQL;
using PX.Objects;
using PX.Objects.GL;
namespace PX.Objects.GL
{
public class JournalEntry_Extension : PXGraphExtension<JournalEntry>
{
public delegate void PopulateSubDescrDelegate(PXCache sender, GLTran Row, Boolean ExternalCall);
[PXOverride]
public void PopulateSubDescr(PXCache sender, GLTran Row, Boolean ExternalCall, PopulateSubDescrDelegate baseMethod)
{
if (Base.IsImport || Base.IsExport || Base.IsContractBasedAPI)
{
return;
}
baseMethod(sender,Row,ExternalCall);
}
}
}

Related

How to retrieve only a predefined number of results in Azure Tables

I am trying to perform an azure table query.
My table (that saves logs) has thousands of rows of data, and it gets populated with more each second.
Right now i have only 1 partition key, but it doesn't affect the next question.
How can i get back lets say only the 100 latest results.
this is my Entity:
public MainServerLogEntity(string Message)
{
this.PartitionKey = "mainserverlogs";
this.RowKey = (DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks).ToString();
this.Message = Message;
this.Date = DateTime.UtcNow;
}
public MainServerLogEntity() { }
public string Message { get; set; }
public DateTime Date { get; set; }
Right now this is the query i am performing inside a web api i have:
[Route("MainServerLogs")]
[HttpGet]
public IEnumerable<MainServerLogEntity> GetMainServerLogs()
{
CloudTable table = AzureStorageHelpers.GetWebApiTable(connectionString, "mainserverlogs");
TableQuery<MainServerLogEntity> query = new TableQuery<MainServerLogEntity>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "mainserverlogs"));
return table.ExecuteQuery(query);
}
But the problem is that i am getting alot of data, and i am requesting this api every few seconds in order to update the ui.
What should i do? is it possible to define in the query that i only want the 100 first rows?
If it is not possible then what other technique should i use?
Try implementing a .Take(100) on the query like so:
[Route("MainServerLogs")]
[HttpGet]
public IEnumerable<MainServerLogEntity> GetMainServerLogs()
{
CloudTable table = AzureStorageHelpers.GetWebApiTable(connectionString, "mainserverlogs");
TableQuery<MainServerLogEntity> query = new TableQuery<MainServerLogEntity>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "mainserverlogs")).Take(100);
return table.ExecuteQuery(query);
}

Unable to Serialize into XML

I am trying to Serialize the content of some text into an XML file (performed when a user saves their selections), and then will later deserialize it (when the user chooses to display their saved selection).
I have been following the following tutorial on serialization.
I have also tried to do this via LINQ to XML but was either getting namespace errors, or the tool returned no errors, but did not work (with the same problem as described below).
The problem I am having is that my code is not returning any errors, but the function is not working (I have a label control that allows me to see that the 'catch' is being returned). I am building the tool in Expression Blend, using C#.
Here is my SaveSelection.cs Class
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Xml.Serialization;
using System.Xml;
namespace DYH
{
public class SaveSelections
{
[XmlAttribute("Title")]
public string Title
{ get; set; }
[XmlElement("Roof")]
public string RoofSelection
{ get; set; }
[XmlElement("Cladding")]
public string CladdingSelection
{ get; set; }
[XmlElement("MixedCladding")]
public string MixedCladdingSelection
{ get; set; }
[XmlElement("FAJ")]
public string FAJSelection
{ get; set; }
[XmlElement("GarageDoor")]
public string GarageDoorSelection
{ get; set; }
[XmlElement("FrontDoor")]
public string FrontDoorSelection
{ get; set; }
}
}
Here is my C# code
// Save Selection Button
private void Button_SaveSelection_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
try
{
// Save selections into the SavedSelections.xml doc
SaveSelections userselection = new SaveSelections();
userselection.Title = TextBox_SaveSelection.Text;
userselection.RoofSelection = Button_Roof_Select.Text;
userselection.CladdingSelection = Button_Cladding_Select.Text;
userselection.MixedCladdingSelection = Button_MixedCladding_Select.Text;
userselection.FAJSelection = Button_FAJ_Select.Text;
userselection.GarageDoorSelection = Button_GarageDoor_Select.Text;
userselection.FrontDoorSelection = Button_FrontDoor_Select.Text;
SerializeToXML(userselection);
// XDocument xmlSaveSelections = XDocument.Load("../SavedSelections.xml");
//
// XElement newSelection = new XElement("Selection", //xmlSaveSelections.Element("Selections").Add(
// //new XElement("Selection",
// new XElement("Title", TextBox_SaveSelection.Text),
// new XElement("RoofSelection", Button_Roof_Select.Text),
// new XElement("CladdingSelection", Button_Cladding_Select.Text),
// new XElement("MixedCladdingSelection", Button_MixedCladding_Select.Text),
// new XElement("FAJSelection", Button_FAJ_Select.Text),
// new XElement("GarageDoorSelection", Button_GarageDoor_Select.Text),
// new XElement("FrontDoorSelection", Button_FrontDoor_Select.Text));
//
//// xmlSaveSelections.Add(newSelection);
//// xmlSaveSelections.Save("../SavedSelections.xml");
SelectionLabel.Text = "Your selection has been saved as " + "'" + TextBox_SaveSelection.Text + "'. We suggest you write down the name of your selection.";
}
catch(Exception ex)
{
throw ex;
SelectionLabel.Text = "There was a problem saving your selection. Please try again shortly.";
}
}
// Saves SaveSelection.cs to XML file SavedSelections.xml
static public void SerializeToXML(SaveSelections selection)
{
XmlSerializer serializer = new XmlSerializer(typeof(SaveSelections));
TextWriter textWriter = new StreamWriter(#"/SavedSelections.xml");
serializer.Serialize(textWriter, selection);
textWriter.Close();
}
I have left evidence of one of my previous attempts commented out so you can see a previous format I tried.
My issue is that when I try to use the tool, the SelectionLabel.Text returns "There was a problem saving your selection. Please try again shortly." so I know that the code is returning the catch and not executing the 'try'.
Any help??
Edit 18/6/2012: The below code was the code that worked as per correct answer to question.
public void Button_SaveSelection_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
string roofSelection = TextBox_SaveSelection.Text + "_RoofSelection";
string claddingSelection = TextBox_SaveSelection.Text + "_CladdingSelection";
string mixedCladdingSelection = TextBox_SaveSelection.Text + "_MixedCladdingSelection";
string fajSelection = TextBox_SaveSelection.Text + "_FAJSelection";
string garageDoorSelection = TextBox_SaveSelection.Text + "_GarageDoorSelection";
string frontDoorSelection = TextBox_SaveSelection.Text + "_FrontDoorSelection";
try
{
using (var store = IsolatedStorageFile.GetUserStoreForApplication())
{
// Gives us 6Mb of storage space in IsoStore
Int64 isoSpaceNeeded = 1048576 * 6;
Int64 currentIsoSpace = store.AvailableFreeSpace;
// If space needed is greater than (>) space available, increase space
if (isoSpaceNeeded > currentIsoSpace)
{
// If user accepts space increase
if (store.IncreaseQuotaTo(currentIsoSpace + isoSpaceNeeded))
{
IsolatedStorageFileStream file = store.CreateFile("SavedSelections.txt");
file.Close();
// Stream writer to populate information in
using (StreamWriter sw = new StreamWriter(store.OpenFile("SavedSelections.txt", FileMode.Open, FileAccess.Write)))
{
appSettings.Add(roofSelection, Button_Roof_Select.Text);
sw.WriteLine(roofSelection);
appSettings.Add(claddingSelection, Button_Cladding_Select.Text);
sw.WriteLine(claddingSelection);
appSettings.Add(mixedCladdingSelection, Button_MixedCladding_Select.Text);
sw.WriteLine(mixedCladdingSelection);
appSettings.Add(fajSelection, Button_FAJ_Select.Text);
sw.WriteLine(fajSelection);
appSettings.Add(garageDoorSelection, Button_GarageDoor_Select.Text);
sw.WriteLine(garageDoorSelection);
appSettings.Add(frontDoorSelection, Button_FrontDoor_Select.Text);
sw.WriteLine(frontDoorSelection);
}
SelectionLabel.Text = "Your selection has been saved as " + "'" + TextBox_SaveSelection.Text + "'. We suggest you write down the name of your selection.";
}
}
}
SelectionLabel.Text = "Your selection has been saved as " + "'" + TextBox_SaveSelection.Text + "'. We suggest you write down the name of your selection.";
}
catch //(Exception ex)
{
//throw ex;
SelectionLabel.Text = "There was a problem saving your selection. Please try again shortly.";
}
}
It looks like your issue is because you're trying to a file, but that file did not come from a FileSaveDialog initiated by a user action. You're running into a security feature of Silverlight where you're not allowed access to the local file system. Instead, try writing to IsolatedStorage. However, be aware that end users can completely (as well as selectively) disable application storage so you'll need to handle those exceptions as well.
Here's a quick article on how to use IsolatedStorage.

How to read last message in MessageQueue

I am trying to read the messages in MessageQueue start from newest one. I know I can use GetAllMessages(). Then loop through. But it is quite slow when there are large amount of messages in the queue. Is there a better way to do this?
Thanks
In MSMQ 3 there is a feature that may be what you're looking for. The ReceiveByLookupId method can get the last message on the queue.
http://msdn.microsoft.com/en-us/library/3w50th9h.aspx
You would use the following action:
MessageLookupAction.Last: Receives the last message in the queue and
removes it from the queue. The lookupId parameter must be set to 0.
If you're using an earlier version of MSMQ you will probably have to use Peek similar to how this blog post explains how to get a count of the message queue. When you got to the last one, you could then ReceiveById.
http://jopinblog.wordpress.com/2008/03/12/counting-messages-in-an-msmq-messagequeue-from-c/
Assuming that the performance hit is in the looping through the messages:
There is no need to loop through all the messages if all you want is the last one. Since GetAllMessages() returns an array of type Message just go to the last element in the array by index.
public class PersonDetails
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateTime { get; set; }
}
const string queueName = #".\private$\PersonQueue";
public void GetMessageFromQueue(string queueName)
{
MessageQueue perosnMessageQueue = new MessageQueue(queueName);
try
{
XmlMessageFormatter xmlMessageFormatter = new XmlMessageFormatter(new Type[] {
(typeof(PersonDetails)) });
perosnMessageQueue.Formatter = xmlMessageFormatter;
perosnMessageQueue.Refresh();
for (int i = perosnMessageQueue.GetAllMessages().Count(); i !=0; i--)
{
var personDetailsFromQueue =
(PersonDetails)perosnMessageQueue.Receive
(MessageQueueTransactionType.Automatic).Body;
Console.WriteLine("FistName : {0} \n LastName : {1} \n Date Time :
{2}",personDetailsFromQueue.FirstName,
personDetailsFromQueue.LastName,
personDetailsFromQueue.DateTime);`
}

Retrieving values of ReadOnly fields from DynamicData DetailsView in Edit Mode on Updating using LinqDataSource

I have several tables in my database that have read-only fields that get set on Inserting and Updating, namely: AddDate (DateTime), AddUserName (string), LastModDate (DateTime), LastModUserName (string).
All of the tables that have these values have been set to inherit from the following interface:
public interface IUserTrackTable
{
string AddUserName { get; set; }
DateTime AddDate { get; set; }
string LastModUserName { get; set; }
DateTime LastModDate { get; set; }
}
As such, I have the following method on the Edit.aspx page:
protected void DetailsDataSource_Updating(object sender, LinqDataSourceUpdateEventArgs e)
{
IUserTrackTable newObject = e.NewObject as IUserTrackTable;
if (newObject != null)
{
newObject.LastModUserName = User.Identity.Name;
newObject.LastModDate = DateTime.Now;
}
}
However, by the time it hits this method, the e.OriginalObject has already lost the values for all four fields, so a ChangeConflictException gets thrown during the actual Update. I have tried adding the four column names to the DetailsView1.DataKeyNames array in the Init event handler:
protected void Page_Init(object sender, EventArgs e)
{
// other things happen before this
var readOnlyColumns = table.Columns.Where(c => c.Attributes.SingleOrDefaultOfType<ReadOnlyAttribute>(ReadOnlyAttribute.Default).IsReadOnly).Select(c => c.Name);
DetailsView1.DataKeyNames = DetailsView1.DataKeyNames.Union<string>(readOnlyColumns).ToArray<string>();
DetailsView1.RowsGenerator = new CustomFieldGenerator(table, PageTemplates.Edit, false);
// other things happen after this
}
I've tried making that code only happen on PostBack, and still nothing. I'm at a lose for how to get the values for all of the columns to make the round-trip.
The only thing the CustomFieldGenerator is handling the ReadOnlyAttribute, following the details on C# Bits.
UPDATE: After further investigation, the values make the round trip to the DetailsView_ItemUpdating event. All of the values are present in the e.OldValues dictionary. However, they are lost by the time it gets to the LinqDataSource_Updating event.
Obviously, there are the "solutions" of making those columns not participate in Concurrency Checks or other ways that involve hard-coding, but the ideal solution would dynamically add the appropriate information where needed so that this stays as a Dynamic solution.
i Drovani, I assume you want data auditing (see Steve Sheldon's A Method to Handle Audit Fields in LINQ to SQL), I would do this in the model in EF4 you can do it like this:
partial void OnContextCreated()
{
// Register the handler for the SavingChanges event.
this.SavingChanges += new EventHandler(context_SavingChanges);
}
private static void context_SavingChanges(object sender, EventArgs e)
{
// handle auditing
AuditingHelperUtility.ProcessAuditFields(objects.GetObjectStateEntries(EntityState.Added));
AuditingHelperUtility.ProcessAuditFields(objects.GetObjectStateEntries(EntityState.Modified), InsertMode: false);
}
internal static class AuditingHelperUtility
{
internal static void ProcessAuditFields(IEnumerable<Object> list, bool InsertMode = true)
{
foreach (var item in list)
{
IAuditable entity = item as IAuditable;
if (entity != null)
{
if (InsertMode)
{
entity.InsertedBy = GetUserId();
entity.InsertedOn = DateTime.Now;
}
entity.UpdatedBy = GetUserId();
entity.UpdatedOn = DateTime.Now;
}
}
}
}
Sadly this is not possible with EF v1

Sharepoint search fails when using DataKeynames

We have a Sharepoint site which uses search.
We get the following error:
Unable to validate data. at
System.Web.Configuration.MachineKeySection.EncryptOrDecryptData
(Boolean fEncrypt, Byte[] buf, Byte[] modifier, Int32 start, Int32 length,
IVType ivType, Boolean useValidationSymAlgo)
at System.Web.UI.ObjectStateFormatter.Deserialize(String inputString)
After a bit of testing we have found that the error occurs when we use DateKeyNames on a GridView control.
Not sure why there should be any combination between Search, this error and DataKeyNames.
Any ideas?
Update: Here is some code
[Guid("8891224e-e830-4ffa-afbd-f789583d8d14")]
public class TestErrorGridView : System.Web.UI.WebControls.WebParts.WebPart
{
Control ascxToAdd;
public TestErrorGridView()
{
}
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
}
protected override void CreateChildControls()
{
base.CreateChildControls();
Table test = new Table();
TestBindObject t1 = new TestBindObject() { ID = 1, Name = "Test" };
List<TestBindObject> l1 = new List<TestBindObject>();
l1.Add(t1);
SPGridView testGrid = new SPGridView() { AutoGenerateColumns = false};
BoundField header = new BoundField();
header.DataField = "ID";
BoundField name = new BoundField();
name.DataField = "Name";
testGrid.Columns.Add(header);
testGrid.Columns.Add(name);
testGrid.DataSource = l1;
testGrid.DataBind();
// If you comment out this line search works
testGrid.DataKeyNames = new string[] { "ID" };
this.Controls.Add(testGrid);
base.CreateChildControls();
SPGridView testGrid = new SPGridView() { AutoGenerateColumns = false, EnableViewState=false };
testGrid.DataKeyNames = new string[] { "testid" };
this.Controls.Add(testGrid);
}
}
public class TestBindObject
{
public int ID { get; set; }
public string Name { get; set; }
}
UPDATE
This error occurrs on the developer machines, so it is not realated to machine keys being different on different machines in a cluster.
One of the developers has MOSS installed, he does not get the error. The developers who have just WSS installed get the error.
UPDATE 2
Have added datasource to code
I'm going to throw out a guess here since the code where you set the GridView's datasource is missing as well, but here goes...
It probably has something to do with the order in which you are setting the GridView's properties. My guess is that you need to set it in this order:
Create your GridView
Set the GridView's datasource (so that it has data)
Set DataKeyNames
Call GridView.DataBind()
Step 3 and 4 are the steps I am not sure about. You may have to DataBind and then set DataKeyNames.
We eventually solved this by following the example in this link:
http://msdn.microsoft.com/en-us/library/bb466219.aspx
I think that is was binding to a data table rather than a list that made the difference.
The fact that Sharepoint grids do not allow autogenerated columns appears to be one of the factors leading to this problem.

Resources