Accessing joined table data - acumatica

Here is the modified code and the error I am getting. Using debug I have confirmed it is happening on the:
var ret = cmd.Select();
Here is the full code.
[PXFilterable]
public PXFilteredProcessing<EDASNShipProj, EDCreateASNFilter> Shipment;
protected virtual IEnumerable shipment()
{
int ii = 0;
foreach (var row in Shipment.Cache.Cached)
{
ii++;
yield return row;
}
if (ii == 0)
{
var cmd = new PXSelectJoin<SOShipment,
LeftJoin<SOOrderShipment, On<SOShipment.shipmentNbr, Equal<SOOrderShipment.shipmentNbr>>,
LeftJoin<SOOrder, On<SOOrder.orderNbr, Equal<SOOrderShipment.orderNbr>>>>,
Where2<Where2<Where2<Where2<Where2<Where2<Where2<
Where<Current<EDCreateASNFilter.customerID>, IsNull,
Or<SOOrderExt.usrEDICustomerId, Equal<Current<EDCreateASNFilter.customerID>>>>,
And<Where<Current<EDCreateASNFilter.startDate>, IsNull,
Or<SOShipment.shipDate, GreaterEqual<Current<EDCreateASNFilter.startDate>>>>>>,
And<Where<SOShipment.shipDate, LessEqual<Current<EDCreateASNFilter.endDate>>>>>,
And<Where<Current<EDCreateASNFilter.shipVia>, IsNull,
Or<SOShipment.shipVia, Equal<Current<EDCreateASNFilter.shipVia>>>>>>,
And<Where<Current<EDCreateASNFilter.truckNbr>, IsNull,
Or<SOShipmentExt.usrTruckNbr, Equal<Current<EDCreateASNFilter.truckNbr>>>>>>,
And<Where<SOShipment.status, Equal<SOShipmentStatus.open>>>>,
And<Where<SOShipmentExt.usrEDIStatus, Equal<SOShipmentEDIStatus.truckAssigned>,
Or<SOShipmentExt.usrEDIStatus, Equal<SOShipmentEDIStatus.newStat>>>>>,
And<Where<SOOrder.customerRefNbr, IsNotNull>>>,
OrderBy<Asc<SOShipment.customerID,
Asc<SOOrderExt.usrEDICustomerId,
Asc<SOOrderExt.usrEDICustomerVendorId,
Asc<SOShipment.shipVia,
Asc<SOShipmentExt.usrTruckNbr,
Asc<SOShipment.customerLocationID>>>>>>>>(this);
cmd.View.Clear();
var ret = cmd.Select();
if (ret != null)
{
EDASNShipProj shipProj = new EDASNShipProj();
foreach (PXResult<SOShipment, SOOrderShipment, SOOrder> record in ret)
{
shipProj = new EDASNShipProj();
SOShipment shipment = (SOShipment)record;
SOShipmentExt soShipmentExt = shipment.GetExtension<SOShipmentExt>();
SOOrder soOrder = (SOOrder)record;
SOOrderExt soOrderExt = soOrder.GetExtension<SOOrderExt>();
shipProj.OrderNbr = soOrder.OrderNbr;
shipProj.CustomerRefNbr = soOrder.CustomerRefNbr;
shipProj.CustomerOrderNbr = soOrder.CustomerOrderNbr;
shipProj.UsrTruckNbr = soShipmentExt.UsrTruckNbr;
shipProj.UsrEDICustomerId = soOrderExt.UsrEDICustomerId;
shipProj.UsrEDICustomerVendorId = soOrderExt.UsrEDICustomerVendorId;
shipProj.UsrEDIStatus = soShipmentExt.UsrEDIStatus;
shipProj.CustomerID = shipment.CustomerID;
shipProj.CustomerLocationID = shipment.CustomerLocationID;
shipProj.ShipVia = shipment.ShipVia;
shipProj.ShipmentNbr = shipment.ShipmentNbr;
shipProj.ShipDate = shipment.ShipDate;
shipProj = Shipment.Insert(shipProj);
Shipment.Cache.SetStatus(shipProj, PXEntryStatus.Held);
yield return shipProj;
}
}
Shipment.Cache.IsDirty = false;

The idea is that Select() returns a PXResultset of the main DAC. You can then loop over this and cast to the joined DACs.
Here is an example :
// The static Select() method is called to execute a BQL command.
PXResultset<OrderDetail> result =
PXSelectJoin<OrderDetail, InnerJoin<SalesOrder,
On<SalesOrder.orderNbr, Equal<OrderDetail.orderNbr>>>>.Select(this);
// Iterating over the result set:
// PXResult should be specialized with the DACs of all joined tables
// to be able to cast to these DACs.
foreach(PXResult<OrderDetail, SalesOrder> record in result)
{
// Casting a result set record to the OrderDetail DAC:
OrderDetail detail = (OrderDetail)record;
// Casting a result set record to the SalesOrder DAC:
SalesOrder order = (SalesOrder)record;
...
}
Please have a look at this article for more information
https://help.acumatica.com/(W(8))/Wiki/ShowWiki.aspx?pageid=8609c829-7b9c-4660-acf9-891b0971b6a3

Look at the type that is inferred when you use var:
'a' is of type 'PXResultset < ARSalesPerTran > ' in this example
If I were to assign it to another variable 'b' of a different type like 'PXResultset < ARSalesPerTran, ARSalesPerTranExt, ARRegister > ' the compiler will report that it can't implicitly cast 'PXResultset < ARSalesPerTran > ' to 'PXResult < ARSalesPerTran, ARSalesPerTranExt, ARRegister > '.
PXResult<ARSalesPerTran, ARSalesPerTranExt, ARRegister> b = a;
However the compiler will allow an explicit cast:
PXResult<ARSalesPerTran, ARSalesPerTranExt, ARRegister> b = (PXResult< ARSalesPerTran, ARSalesPerTranExt, ARRegister>)a;
In a nutshell, this is related to an artifact of C# inferred type system (var) rather than an Acumatica feature.

Related

Why doesn't my BLC 'save' action save all records unless its at the very end of the cache insert?

I have BLC code that is parsing out the AuditHistory 'ModifiedFields' field so that I have multiple lines with separate 'Field' and 'Value' fields. The 'ModifiedFields' field contains null separated values, so my BLC C# code uses the split function to put these into an array. This all works fine. The problem I'm having is saving to a table in Acumatica via the Graph / Cache 'insert' function. If I use the 'Actions.PressSave()' method after every iteration of the array, it doesn't save every record - effectively skipping records. I have no idea why this would happen. If I put the 'Actions.PressSave()' method at the very end of everything, I get all the records - but sometimes it times out, I'm assuming because of (in some cases) the massive amount of records being cached before the save.
Putting the PressSave method at ANY other point in the loop(s) results in missed records.
Here is my BLC code (note the several places I placed the PressSave method for testing, but commented out - leaving the last one):
public PXAction<AUAuditSetup> CreateAuditRecords;
[PXProcessButton]
[PXUIField(DisplayName = "Create Audit Records", MapEnableRights = PXCacheRights.Update, MapViewRights = PXCacheRights.Update)]
protected virtual IEnumerable createAuditRecords(PXAdapter adapter)
{
PXLongOperation.StartOperation(Base, delegate ()
{
//Create graph of TAC screen...
var osdm = PXGraph.CreateInstance<OpenSourceDataMaint>();
xTACOpenSourceDetail osd;
int recordID = 1;
//Get records from AuditHistory
//PXResultset<AuditHistory> res = PXSelect<AuditHistory>.Select(Base);
PXResultset<AuditHistory> res = PXSelect<AuditHistory,
Where<AuditHistory.changeDate, GreaterEqual<Required<AuditHistory.changeDate>>>>.Select(Base, Convert.ToDateTime("01/01/2013"));
var companyID = Convert.ToString(PX.Common.PXContext.GetSlot<int?>("singleCompanyID"));
foreach (PXResult<AuditHistory> rec in res)
{
var ah = (AuditHistory)rec;
if (ah != null)
{
string[] fields = ah.ModifiedFields.Split('\0');
for (int i = 0; i < fields.GetUpperBound(0); i+=2)
{
osd = new xTACOpenSourceDetail();
osd.OpenSourceName = "AuditHistoryTable";
osd.DataID = "1";
osd.String01 = Convert.ToString(PX.Common.PXContext.GetSlot<int?>("singleCompanyID"));
osd.Number01 = ah.BatchID;
osd.Number02 = ah.ChangeID;
osd.Number03 = recordID;
osd.String02 = ah.ScreenID;
osd.String03 = Convert.ToString(ah.UserID);
osd.Date01 = ah.ChangeDate;
osd.String04 = ah.Operation;
osd.String05 = ah.TableName;
osd.String06 = ah.CombinedKey;
osd.String07 = fields[i];
osd.String08 = fields[i + 1];
osd.String09 = Convert.ToString(ah.ChangeDate);
osdm.OpenSourceDataDetail.Insert(osd);
//if (osd != null)
//osdm.Actions.PressSave();
recordID++;
}
recordID = 1;
//osdm.Actions.PressSave();
}
//osdm.Actions.PressSave();
}
osdm.Actions.PressSave();
});
return adapter.Get();
}
Any ideas?
A couple of things you could try:
Instead of instantiating osd directly, replace with:
osd = osdm.OpenSourceDataDetail.Insert();
and then after assigning values and prior to PressSave(), use .Update on the cache:
osdm.OpenSourceDataDetail.Update(osd);
and then after PressSave(), clear the graph:
osdm.Clear();
Importantly, does your DAC have an IsKey = true on the DataID field and marked PXDBIdentity, hence auto-assigned and unique?
Example:
public abstract class dataID : PX.Data.IBqlField {}
[PXDBIdentity(IsKey = true)]
[PXUIField(Visible = false, Enabled = false)]
public virtual int? DataID {get; set;}
If the changes are successful, you could then try moving the .PressSave() and .Clear() after the For loop.

How to get Opportunity Relations in Sales Order

In Opportunity screen, the definition of the data view for Relations is simply :
public CRRelationsList<CROpportunity.noteID> Relations;
When a Sales Order is raised from the Opportunity. I'd like to display the Relations defined from the source Opporunity in another tab. And I'm just struggling how to write the the data view and pass the Opportunity noteid.
public CRRelationsList<???>Relations;
Thanks !
The generic type in dataviews often resolve to the current record.
In CRRelationsList class the generic type is named TNoteField:
public class CRRelationsList<TNoteField> : PXSelect<CRRelation>
where TNoteField : IBqlField
ssuming the dataview is declared as CRRelationsList<CROpportunity.noteID>.
The generic type value will be resolved like this Caches[typeof(CROpportunity)].Current.NoteID.
protected virtual void CRRelation_RefNoteID_FieldDefaulting(PXCache sender, PXFieldDefaultingEventArgs e)
{
// Get a cache object of type CROpportunity
var refCache = sender.Graph.Caches[BqlCommand.GetItemType(typeof(TNoteField))];
// Get the NoteID field value of the current CROpportunity object
e.NewValue = refCache.GetValue(refCache.Current, typeof(TNoteField).Name);
}
So to set DAC.Field of CRelationsList<DAC.field> you would do:
// In a graph extension (PXGraphExtension)
Base.Caches[typeof(DAC)].Current.Fied = ???;
// Or in graph (PXGraph)
Caches[typeof(DAC)].Current.Fied = ???;
If current DAC object is null you need to insert a record in a dataview or directly in the cache object.
I'm not sure re-using CRRelationsList list is the best approach if you want to simply display records because it does much more than that. It should be possible to extract the select request out of it and directly substitute the TNoteField value:
private static PXSelectDelegate GetHandler()
{
return () =>
{
var command = new Select2<CRRelation,
LeftJoin<BAccount, On<BAccount.bAccountID, Equal<CRRelation.entityID>>,
LeftJoin<Contact,
On<Contact.contactID, Equal<Switch<Case<Where<BAccount.type, Equal<BAccountType.employeeType>>, BAccount.defContactID>, CRRelation.contactID>>>,
LeftJoin<Users, On<Users.pKID, Equal<Contact.userID>>>>>,
Where<CRRelation.refNoteID, Equal<Current<TNoteField>>>>();
var startRow = PXView.StartRow;
int totalRows = 0;
var list = new PXView(PXView.CurrentGraph, false, command).
Select(null, null, PXView.Searches, PXView.SortColumns, PXView.Descendings, PXView.Filters,
ref startRow, PXView.MaximumRows, ref totalRows);
PXView.StartRow = 0;
foreach (PXResult<CRRelation, BAccount, Contact, Users> row in list)
{
var relation = (CRRelation)row[typeof(CRRelation)];
var account = (BAccount)row[typeof(BAccount)];
relation.Name = account.AcctName;
relation.EntityCD = account.AcctCD;
var contact = (Contact)row[typeof(Contact)];
if (contact.ContactID == null && relation.ContactID != null &&
account.Type != BAccountType.EmployeeType)
{
var directContact = (Contact)PXSelect<Contact>.
Search<Contact.contactID>(PXView.CurrentGraph, relation.ContactID);
if (directContact != null) contact = directContact;
}
relation.Email = contact.EMail;
var user = (Users)row[typeof(Users)];
if (account.Type != BAccountType.EmployeeType)
relation.ContactName = contact.DisplayName;
else
{
if (string.IsNullOrEmpty(relation.Name))
relation.Name = user.FullName;
if (string.IsNullOrEmpty(relation.Email))
relation.Email = user.Email;
}
}
return list;
};
}

How can I save a custom field on ARCashSale after the Release process is complete?

When I release a Cash Sales document, after everything verifies successfully and the GL Batch is created, I execute code which created a second GL Batch that handles other transactions related to the cash sale. After that batch is created, I want to save the second GL Batch's Ref Nbr on the Cash Sales document as well. When I try to save it, I get an error:
Here is my code overriding the normal Release process:
public delegate IEnumerable ReleaseDelegate(PXAdapter adapter);
[PXOverride]
public IEnumerable Release(PXAdapter adapter, ReleaseDelegate baseMethod)
{
ARCashSale cashSale = Base.Document.Current;
PXGraph.InstanceCreated.AddHandler<JournalEntry>(delegate (JournalEntry oldJournalEntry)
{
oldJournalEntry.RowPersisted.AddHandler<Batch>(delegate (PXCache sender, PXRowPersistedEventArgs e)
{
Batch oldBatch = oldJournalEntry.BatchModule.Current;
if (oldBatch != null && isCreated == false && e.Operation == PXDBOperation.Insert && e.TranStatus == PXTranStatus.Completed)
{
isCreated = true;
if (CFBSAdjustments.Select().Count > 0)
{
JournalEntry newJournalEntry = PXGraph.CreateInstance<JournalEntry>();
Batch newBatch = new Batch();
newBatch = newJournalEntry.BatchModule.Insert(newBatch);
Customer customer = PXSelect<Customer, Where<Customer.bAccountID, Equal<Required<Customer.bAccountID>>>>.Select(Base, cashSale.CustomerID);
newBatch.Description = "Fund Entry for Cash Sales Reference " + cashSale.RefNbr;
newBatch.FinPeriodID = oldBatch.FinPeriodID;
newBatch.LedgerID = oldBatch.LedgerID;
newBatch.DateEntered = oldBatch.DateEntered;
decimal? debit = 0;
decimal? credit = 0;
foreach (CFBSCashSalesAdjustment row in CFBSAdjustments.Select())
{
GLTran tran = newJournalEntry.GLTranModuleBatNbr.Insert();
tran.SummPost = true;
tran.BranchID = cashSale.BranchID;
tran.TranType = GLTran.tranClass.Normal;
tran.TranClass = GL.Messages.RoundingDiff;
tran.LedgerID = newBatch.LedgerID;
tran.FinPeriodID = newBatch.FinPeriodID;
tran.TranDate = newBatch.DateEntered;
tran.CuryInfoID = Base.currencyinfo.Current.CuryInfoID;
tran.AccountID = row.Account;
tran.SubID = row.Subaccount;
tran.DebitAmt = row.DebitAmt;
tran.CuryDebitAmt = row.DebitAmt;
debit += row.DebitAmt;
tran.CreditAmt = row.CreditAmt;
tran.CuryCreditAmt = row.CreditAmt;
credit += row.CreditAmt;
tran.RefNbr = row.CashSalesRefNbr;
tran.TranDesc = customer.AcctCD + " - " + customer.AcctName;
newJournalEntry.GLTranModuleBatNbr.Update(tran);
}
newBatch = newJournalEntry.BatchModule.Update(newBatch);
if (GLSetup.Current.GetExtension<GLSetupExt>().UsrAutoRelease == true)
{
newJournalEntry.BatchModule.Current.Hold = false;
newJournalEntry.release.Press();
}
newJournalEntry.Save.Press();
if (isCreated)
isCreated = false;
cashSale.GetExtension<ARRegisterExt>().UsrFundBatch = newJournalEntry.BatchModule.Current.BatchNbr;
//Base.Document.Current.GetExtension<ARRegisterExt>().UsrFundBatch = newJournalEntry.BatchModule.Current.BatchNbr;
//Base.dummy_CATran.View.RequestRefresh();
Base.Document.Update(cashSale);
//TODO - Figure out why the fund batch isn't saving to the Cash Sale
Base.Actions.PressSave();
//Base.dummy_CATran.Cache.ClearQueryCache();
//Base.Persist(typeof(ARCashSale), PXDBOperation.Update);
}
}
});
});
return baseMethod(adapter);
}
I left in all of the different methods I've tried to get the ref nbr to save. I've ever tried to add a field updated handler for the BatchNbr field and force the new number in that way, but it did not work.
EDIT: I also noticed that I cannot successfully set an extended field on the Batch DAC. For example, I have the line newBatch.GetExtension<BatchExt>().ExtRefNbr = cashSale.RefNbr; to set the ref nbr in that document as well, but any time I run 'NewJournal.Press.Save()' it changes the set value of the extended field to null. If anyone knows how to set the extension field, I may be able to work with that instead to go down a different path that may do what I need.
I believe the appropriate place to perform your update is in graph ARDocumentRelease. Try something like this....
public class ARDocumentReleaseExtension : PXGraphExtension<ARDocumentRelease>
{
public override void Initialize()
{
ARSetup setup = Base.arsetup.Current;
Base.ARDocumentList.SetProcessDelegate(
delegate (List<BalancedARDocument> list)
{
List<ARRegister> newlist = new List<ARRegister>(list.Count);
foreach (BalancedARDocument doc in list)
{
newlist.Add(doc);
}
ARDocumentRelease.ReleaseDoc(newlist, true);
CreateSecondBatch(newlist);
}
);
Base.ARDocumentList.SetProcessCaption(PX.Objects.AR.Messages.Release);
Base.ARDocumentList.SetProcessAllCaption(PX.Objects.AR.Messages.ReleaseAll);
}
private void CreateSecondBatch(List<ARRegister> docs)
{
foreach(BalancedARDocument register in docs)
{
if (register.DocType == ARDocType.CashSale)
{
//create the second batch and assign the 2nd ref nbr to the cash sale user field
}
}
}
}

Return a set of objects from a class

I have a method that adds a new item to an EF table, then queries back the table to return a subset of the table. It needs to return to the caller a set of "rows", each of which is a set of columns. I'm not sure how to do this. I have some code, but I think it's wrong. I don't want to return ONE row, I want to return zero or more rows. I'm not sure what DataType to use... [qryCurrentTSApproval is an EF object, referring to a small view in SS. tblTimesheetEventlog is also an EF object, referring to the underlying table]
Ideas?
private qryCurrentTSApproval LogApprovalEvents(int TSID, int EventType)
{
using (CPASEntities ctx = new CPASEntities())
{
tblTimesheetEventLog el = new tblTimesheetEventLog();
el.TSID = TSID;
el.TSEventType = EventType;
el.TSEUserName = (string)Session["strShortUserName"];
el.TSEventDateTime = DateTime.Now;
ctx.tblTimesheetEventLogs.AddObject(el);
ctx.AcceptAllChanges();
var e = (from x in ctx.qryCurrentTSApprovals
where x.TSID == TSID
select x);
return (qryCurrentTSApproval)e;
}
}
Change your method return type to a collection of qryCurrentTSApproval
private List<qryCurrentTSApproval> LogApprovalEvents(int TSID, int EventType)
{
using (CPASEntities ctx = new CPASEntities())
{
// some other existing code here
var itemList = (from x in ctx.qryCurrentTSApprovals
where x.TSID == TSID
select x).ToList();
return itemList;
}
}

Entity Framework - IQueryable to DataTable

I'm trying to convert an IQueryable object to a DataTable. Here's an example of a query that I would like to convert to a DataTable:
var query = DbContext.SomeObjectSet.Select(x => new { PropertyName1 = x.ColumnName1, PropertyName2 = x.ColumnName2 });
Please note the anonymous object that is created in the Select method and the names of the properties:
new { PropertyName1 = x.ColumnName1, PropertyName2 = x.ColumnName2 }
After Googling this issue, I came across the following code that converts an IQueryable object to a DataTable:
public static DataTable EntityToDatatable(this IQueryable result)
{
ObjectQuery query = (result as ObjectQuery);
ObjectContext context = query.Context;
EntityConnection entityCon = (context.Connection as EntityConnection);
using (SqlConnection sqlCon = new SqlConnection(entityCon.StoreConnection.ConnectionString))
{
using (SqlCommand cmd = new SqlCommand(query.ToTraceString(), sqlCon))
{
foreach (var param in query.Parameters)
{
cmd.Parameters.AddWithValue(param.Name, param.Value);
}
using (SqlDataAdapter dataAdapter = new SqlDataAdapter(cmd))
{
using (DataTable dataTable = new DataTable())
{
dataAdapter.Fill(dataTable);
return dataTable;
}
}
}
}
}
The code above "works" and the SQL statement from the ToTraceString() method is as follows:
SELECT [Extent1].[ColumnName1] AS [ColumnName1], [Extent1].[ColumnName2] AS [ColumnName2] FROM [dbo].[TableName] AS [Extent1]
Problem: The column names of the SQL statement (i.e. columnName1 and columnName2) do not correspond to the names of the properties of the objects (i.e. PropertyName1 and PropertyName2) that would be materialized if a ToList() or AsEnumerable() method was called on the query. This wouldn't be so bad if the SQL statement columns were in the same order as the anonymous object properties...but, this is not always the case. Somewhere (I guess inside of the IQueryable object) there must be a mapping between the SQL statement column names and the resulting anonymous object property names.
Does anyone know how to get at this mapping?
I've managed to find a solution to my problem:
First, you need the following code (from How does Entity Framework manage mapping query result to anonymous type?) which maps the positions of my anonymous object properties to the SQL statement column position:
public static Int32[] GetPropertyPositions(this ObjectQuery query)
{
// get private ObjectQueryState ObjectQuery._state;
// of actual type internal class
// System.Data.Objects.ELinq.ELinqQueryState
Object queryState = GetProperty(query, "QueryState");
AssertNonNullAndOfType(queryState, "System.Data.Objects.ELinq.ELinqQueryState");
// get protected ObjectQueryExecutionPlan ObjectQueryState._cachedPlan;
// of actual type internal sealed class
// System.Data.Objects.Internal.ObjectQueryExecutionPlan
Object plan = GetField(queryState, "_cachedPlan");
AssertNonNullAndOfType(plan, "System.Data.Objects.Internal.ObjectQueryExecutionPlan");
// get internal readonly DbCommandDefinition ObjectQueryExecutionPlan.CommandDefinition;
// of actual type internal sealed class
// System.Data.EntityClient.EntityCommandDefinition
Object commandDefinition = GetField(plan, "CommandDefinition");
AssertNonNullAndOfType(commandDefinition, "System.Data.EntityClient.EntityCommandDefinition");
// get private readonly IColumnMapGenerator EntityCommandDefinition._columnMapGenerator;
// of actual type private sealed class
// System.Data.EntityClient.EntityCommandDefinition.ConstantColumnMapGenerator
Object columnMapGenerator = GetField(commandDefinition, "_columnMapGenerator");
AssertNonNullAndOfType(columnMapGenerator, "System.Data.EntityClient.EntityCommandDefinition+ConstantColumnMapGenerator");
// get private readonly ColumnMap ConstantColumnMapGenerator._columnMap;
// of actual type internal class
// System.Data.Query.InternalTrees.SimpleCollectionColumnMap
Object columnMap = GetField(columnMapGenerator, "_columnMap");
AssertNonNullAndOfType(columnMap, "System.Data.Query.InternalTrees.SimpleCollectionColumnMap");
// get internal ColumnMap CollectionColumnMap.Element;
// of actual type internal class
// System.Data.Query.InternalTrees.RecordColumnMap
Object columnMapElement = GetProperty(columnMap, "Element");
AssertNonNullAndOfType(columnMapElement, "System.Data.Query.InternalTrees.RecordColumnMap");
// get internal ColumnMap[] StructuredColumnMap.Properties;
// array of internal abstract class
// System.Data.Query.InternalTrees.ColumnMap
Array columnMapProperties = GetProperty(columnMapElement, "Properties") as Array;
AssertNonNullAndOfType(columnMapProperties, "System.Data.Query.InternalTrees.ColumnMap[]");
Int32 n = columnMapProperties.Length;
Int32[] propertyPositions = new Int32[n];
for (Int32 i = 0; i < n; ++i)
{
// get value at index i in array
// of actual type internal class
// System.Data.Query.InternalTrees.ScalarColumnMap
Object column = columnMapProperties.GetValue(i);
AssertNonNullAndOfType(column, "System.Data.Query.InternalTrees.ScalarColumnMap");
//string colName = (string)GetProp(column, "Name");
// can be used for more advanced bingings
// get internal int ScalarColumnMap.ColumnPos;
Object columnPositionOfAProperty = GetProperty(column, "ColumnPos");
AssertNonNullAndOfType(columnPositionOfAProperty, "System.Int32");
propertyPositions[i] = (int)columnPositionOfAProperty;
}
return propertyPositions;
}
static object GetProperty(object obj, string propName)
{
PropertyInfo prop = obj.GetType().GetProperty(propName, BindingFlags.NonPublic | BindingFlags.Instance);
if (prop == null) throw EFChangedException();
return prop.GetValue(obj, new object[0]);
}
static object GetField(object obj, string fieldName)
{
FieldInfo field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
if (field == null) throw EFChangedException();
return field.GetValue(obj);
}
static void AssertNonNullAndOfType(object obj, string fullName)
{
if (obj == null) throw EFChangedException();
string typeFullName = obj.GetType().FullName;
if (typeFullName != fullName) throw EFChangedException();
}
static InvalidOperationException EFChangedException()
{
return new InvalidOperationException("Entity Framework internals has changed, please review and fix reflection code");
}
Then I can modify the EntityToDatatable method as follows:
public static DataTable EntityToDatatable(this IQueryable query)
{
SqlConnection sqlConnection = null;
SqlCommand sqlCommand = null;
SqlDataAdapter sqlDataAdapter = null;
DataTable dataTable = null;
try
{
ObjectQuery objectQuery = (query as ObjectQuery);
ObjectContext objectContext = objectQuery.Context;
EntityConnection entityConnection = (objectContext.Connection as EntityConnection);
sqlConnection = new SqlConnection(entityConnection.StoreConnection.ConnectionString);
sqlCommand = new SqlCommand(objectQuery.ToTraceString(), sqlConnection);
foreach (var parameter in objectQuery.Parameters)
{
sqlCommand.Parameters.AddWithValue(parameter.Name, parameter.Value);
}
sqlDataAdapter = new SqlDataAdapter(sqlCommand);
dataTable = new DataTable();
sqlDataAdapter.Fill(dataTable);
// Get the mapping between the object property position and
// the SQL statment column position.
Int32[] propertyPositions = objectQuery.GetPropertyPositions();
// Create a column name to column position (ordinal) lookup.
Dictionary<String, Int32> mapColumnNameToColumnPosition = new Dictionary<string, int>();
// Populate the lookup.
for (Int32 i = 0; i < propertyPositions.Length; ++i)
{
mapColumnNameToColumnPosition.Add(dataTable.Columns[propertyPositions[i]].ColumnName, i);
}
// Get the object's property information.
PropertyInfo[] pi = query.GetType().GetGenericArguments()[0].GetProperties();
// Iterate through the lookup and change the position of the datatable columns.
// The order of the datatable columns will now correspond to the order of the object
// properties.
foreach (var map in mapColumnNameToColumnPosition)
{
// Change the column position.
dataTable.Columns[map.Key].SetOrdinal(map.Value);
// Change the column name.
dataTable.Columns[map.Key].ColumnName = pi[map.Value].Name;
}
return dataTable;
}
catch (Exception ex)
{
// Something went wrong and we're going to raise an exception...we
// might as well dispose of the datatable if it exists because it's
// not going to be used.
if (dataTable != null) dataTable.Dispose();
throw new Exception("IQueryable to DataTable conversion error.", ex);
}
finally
{
// Do some cleanup on objects that are no longer needed.
if (sqlDataAdapter != null) sqlDataAdapter.Dispose();
if (sqlCommand != null) sqlCommand.Dispose();
if (sqlConnection != null) sqlConnection.Dispose();
}
}

Resources