- Home
- LLBLGen Pro
- Architecture
Recommendation for populating models in v5
Joined: 23-Jan-2005
I'm trying to understand the new features in v5 and looking for current recommendations/approaches on populating a class/model/DTO.
Given this as a example for the model:
public class CustomerOrderModel
{
public Int32 OrderId { get; set; }
public String CustomerId { get; set; }
public Nullable<Int32> EmployeeId { get; set; }
public Nullable<DateTime> OrderDate { get; set; }
public Nullable<DateTime> RequiredDate { get; set; }
public Nullable<DateTime> ShippedDate { get; set; }
public Nullable<Int32> ShipVia { get; set; }
public Nullable<Decimal> Freight { get; set; }
public String ShipCity { get; set; }
public String ShipRegion { get; set; }
public String ShipCountry { get; set; }
public String CompanyID { get; set; }
public String CompanyName { get; set; }
public String SalesRepLastName { get; set; }
public String SalesRepFirstName { get; set; }
}
And this as the SQL:
SELECT
o.OrderID
,o.OrderDate
,o.RequiredDate
,o.ShippedDate
,c.CustomerID
,c.CompanyName
,o.EmployeeId
,e.LastName As SalesRepLastName
,e.FirstName As SalesRepFirstName
,o.ShipVia
,o.Freight
,o.ShipCity
,o.ShipRegion
,o.ShipCountry
FROM
dbo.Customers c
INNER JOIN dbo.Orders o
ON c.CustomerID = o.CustomerID
INNER JOIN dbo.Employees e
ON e.EmployeeID = o.EmployeeID
In v4 and previous, I would've created an entity based on a SQL database view. I could've either used the entity directly or populated a model using some type of mapping code or mapper tool (AutoMapper/Mapster, etc.).
What would be some good approaches for doing this? I'm interested in reducing the amount of initial coding as well as what works for future maintenance.
Joined: 28-Nov-2005
Well, now you can define derived models on top of the entity model and target a different framework in each derived model. You can use the derived models to produce hierarchical projected, denormalized sets and e.g. store them in document databases or use them as DTOs in MVC models or exposed as a service to client-side frameworks. Support for several document databases (MongoDB, RavenDB and a generic document DB) has been added, as well as readonly / readwrite DTO projections. (more info...).
In the case you exposed, you could still map a DB view to an entity and define a derived model on top of that.
Joined: 25-Oct-2005
Old thread I know but I've got some questions on the subject.
For a LINQ-Head like me the designer provides two options for generating DTOs from entities: **Derived Models **and Typed Lists.
A comparision:
- **Derived Models ** Typed Lists
- New in V5 Been there for a while
- Currently LINQ only LINQ, QuerySpec, DataTable
- Supports updates back to entity Entity to DTO only
- Single Entity to DTO as well as IQueryable IQueryable to DTOs only
- Put in a separate VS project from entities In same VS project as entities
- Can filter by any related field Can only filter by fields in the DTO
- Select only SelectMany (i.e can have multiple rows per entity)
- Nice UX Not so nice UX What other differences are there? The only advantage that Typed Lists has that I can see is that it does SelectMany otherwise Derived Models are the way to go - thoughts?
Joined: 28-Nov-2005
IMHO, TypedLists are great. However now with DerivelModels I prefer to use DTOs, as they are more flexible, organic and standard across layers. They are more intuitive for navigation and they are best serialized for the json libraries out there.
Joined: 17-Aug-2003
Typed lists are flat lists, so a joined set. Derived model elements are hierarchies and not flat lists. This is a fundamental difference which IMHO already makes the rest of the comparisons rather moot
If you however denormalize all fields in a Derived model element to make it a flat list, then it's quite similar indeed.
Joined: 25-Oct-2005
Ha I indeed left out the most fundamental difference.
Though what I didn't state was that I was only considering options for creating a flat list DTO. Given my flat list focus the main difference seems that Derived Models are limited to only denormalizing fields in entities with X-to-1 relationships with the Root Entity but fields in entities in a 1-to-many relationships with the Root Entity can't be denormalized. Any plans to offer that for Derived Models? Or will we always have to use Typed Lists for that?
Joined: 17-Aug-2003
Indeed denormalizing fields in DTOs is only for fields accessible over 1:1/m:1 relationships. This is by design as the way the DTOs are fetched is using a query which is potentially a hierarchical query (nested queries in projection). A typed list is always a joined set. The DTO approach has the advantage that it doesn't give you duplicate rows which can be less efficient in some scenarios (e.g. skewed 1:n relationships with many related elements per parent) but has the disadvantage that denormalizing a field over an 1:n relationship is potentially very wasteful as the related query is a nested query and you'll get the problem where you have to filter child rows on parent and child as a set which has its limits (parameters).
So each has its pros/cons, where DTOs are mainly designed to be used hierarchical, like document graphs, where you denormalize fields when needed. Typed lists are designed to be just as flat lists and never as hierarchies. The API for typedlists was designed way earlier than the one for DTOs and the API for typedlists is meant to be used side-by-side with entities, they're part of the entity model and typedlists are seen as 'projections on the entity model' in the same scope. DTO models however are projections of models on top of the entity model and projections of these are outside the scope of the entity model.
In practice, if you create a flat list from DTOs then it doesn't look that much different (except perhaps the API), but through its core design to be hierarchical, it has a limitation with what you can do if you want them to appear as flat lists.
Joined: 25-Oct-2005
Otis wrote:
In practice, if you create a flat list from DTOs then it doesn't look that much different (except perhaps the API), but through its core design to be hierarchical, it has a limitation with what you can do if you want them to appear as flat lists.
In providing data for our reports (we use Microsoft.ReportViewer to render our reports) we often require completely flat lists i.e. one row per child row resulting in duplicate parent data.
Currently we hand code those report objects and the LINQ projections on to them. Seems to me we could start using typed lists for that. (if we buy LLBLGen licenses for everyone ) The main thing stopping us doing that is that the typed list LINQ API is not as flexible as the Derived model API. Particularly that since you can only filter after a projection then you can only filter on fields present in the typedlist. If the projection was separated from datasource we could filter the datasource before the projection.
One solution would be to change this generated method:
/// <summary>Gets the query to fetch the typed list Risk.HazardGridModel</summary>
/// <returns>IQueryable</returns>
public IQueryable<AQD.Model.TypedListClasses.HazardGridModelRow> GetHazardGridModelTypedList()
{
var current0 = this.Hazard;
to
/// <summary>Gets the query to fetch the typed list Risk.HazardGridModel</summary>
/// <returns>IQueryable</returns>
public IQueryable<HazardGridModelRow> GetHazardGridModelTypedList(IQueryable<HazardEntity> current0)
{
current0 = current0 ?? this.Hazard;
var current1 = from risk_Hazard in current0
i.e., We can override this.Hazard with a passed in IQueryable<HazardEntity> with arbitrary filters on it.
If you follow what I'm saying.
Joined: 17-Aug-2003
Ok, and how do you like to obtain the initial queryable (without the projection) ? As I understand you correctly, you'd like to get 2 methods: 1 which produces the IQueryable<T> with just the joins, and 1 which receives that IQueryable<T> and projects it to the typedlist class, am I correct ?
The method as we have it now can't go, that'll break a lot of code. What can be done is make it call 2 methods, (one to create the joins, and one to project the joins) so you have the choice. Problem with that is that it leads to 2 extra methods per typed list and we're currently working on reducing the amount of generated code
Joined: 25-Oct-2005
Otis wrote:
Ok, and how do you like to obtain the initial queryable (without the projection) ? As I understand you correctly, you'd like to get 2 methods: 1 which produces the IQueryable<T> with just the joins, and 1 which receives that IQueryable<T> and projects it to the typedlist class, am I correct ?
No - though that would work for me too .
No I'm sugesting just adding a parameter to the existing method, so from my example above, being able to call
LinqMetaData.GetHazardGridModelTypedList(LinqMetaData.Hazard.Where(h=h.State="SomeState"))
I substitute the root queryable BEFORE all the joins.
Being able to put in code between the joined IQueryable and the projection would probably be handy but that's a bigger change, for the reasons you pointed out.
For reference
/// <summary>Gets the query to fetch the typed list Risk.HazardGridModel</summary>
/// <returns>IQueryable</returns>
public IQueryable<AQD.Model.TypedListClasses.HazardGridModelRow> GetHazardGridModelTypedList()
{
var current0 = this.Hazard;
// I want to come in here
var current1 = from risk_Hazard in current0
join general_OrganisationStructurePortal in this.OrganisationStructurePortal on risk_Hazard.BusinessUnitAffectedID equals general_OrganisationStructurePortal.OrgItemID into joinresult_1
from general_OrganisationStructurePortal in joinresult_1.DefaultIfEmpty()
join general_StaffMember in this.StaffMember on risk_Hazard.EnteredByID equals general_StaffMember.StaffMemberID into joinresult_2
from general_StaffMember in joinresult_2.DefaultIfEmpty()
join lastReviewedBy in this.StaffMember on risk_Hazard.LastReviewedByID equals lastReviewedBy.StaffMemberID into joinresult_3
from lastReviewedBy in joinresult_3.DefaultIfEmpty()
join locationDescriptor in this.HazardRiskDescriptor on risk_Hazard.HazardLocationID equals locationDescriptor.ItemID into joinresult_4
from locationDescriptor in joinresult_4.DefaultIfEmpty()
join risk_HazardRiskDescriptor in this.HazardRiskDescriptor on risk_Hazard.ClassificationID equals risk_HazardRiskDescriptor.ItemID into joinresult_5
from risk_HazardRiskDescriptor in joinresult_5.DefaultIfEmpty()
join risk_HazardStatus in this.HazardStatus on risk_Hazard.HazardStatusID equals risk_HazardStatus.HazardStatusID into joinresult_6
from risk_HazardStatus in joinresult_6.DefaultIfEmpty()
select new {general_OrganisationStructurePortal, risk_Hazard, general_StaffMember, lastReviewedBy, locationDescriptor, risk_HazardRiskDescriptor, risk_HazardStatus };
// Not here
return current1.Select(v=>new AQD.Model.TypedListClasses.HazardGridModelRow() { EnteredBy = v.general_StaffMember.StaffMemberName, DateIdentified = v.risk_Hazard.DateIdentified, EnteredOn = v.risk_Hazard.EnteredOn, HazardNo = v.risk_Hazard.HazardNo, HazardNoSortable = v.risk_Hazard.HazardNoSortable, Category = v.risk_HazardRiskDescriptor.ItemText, HazardTitle = v.risk_Hazard.HazardTitle, AffectedBusinessUnit = v.general_OrganisationStructurePortal.OrgItemDescription, Location = v.locationDescriptor.ItemText, HazardStatus = v.risk_HazardStatus.HazardStatus, NextReviewDate = v.risk_Hazard.NextReviewDate, BusinessUnitAffectedID = v.risk_Hazard.BusinessUnitAffectedID, LastReviewedBy = v.lastReviewedBy.StaffMemberName, LastReviewedDate = v.risk_Hazard.LastReviewedDate });
}
Joined: 17-Aug-2003
Ok, though that limits the amount of filtering you can apply, e.g. in your example you can only filter on Hazard, not on risk_Hazard for example. But it could be an option indeed: an optional parameter which is by default null and if not null, will be assigned to current0 instead of the default one.
Joined: 25-Oct-2005
Otis wrote:
Ok, though that limits the amount of filtering you can apply, e.g. in your example you can only filter on Hazard, not on risk_Hazard for example.
?Not following you - risk_Hazard is the variable for Hazard.
Joined: 25-Oct-2005
Otis wrote:
My bad, I meant one of the joined entities, like StaffMember. I picked a random name in the join list and didn't look closely enough
Funnily that example does work - in LinqPad
GetHazardGridModelTypedList(Hazard.Where(h=>h.StaffMember.StaffMemberID==136))
SELECT [LPA_L1].[StaffMemberName] AS [EnteredBy],
[LPA_L1].[DateIdentified],
[LPA_L1].[EnteredOn],
[LPA_L1].[HazardNo],
[LPA_L1].[HazardNoSortable],
[LPA_L1].[ItemText7] AS [Category],
[LPA_L1].[HazardTitle],
[LPA_L1].[OrgItemDescription] AS [AffectedBusinessUnit],
[LPA_L1].[BusinessUnitAffectedID],
[LPA_L1].[ItemText] AS [Location],
[LPA_L1].[HazardStatus],
[LPA_L1].[NextReviewDate],
CASE
WHEN CASE
WHEN ([LPA_L1].[NextReviewDate] < '2018-01-12T16:07:18' /* @p1 */) THEN 1
ELSE 0
END = 1 THEN 'zoverdue' /* @p3 */
ELSE
CASE
WHEN CASE
WHEN (([LPA_L1].[NextReviewDate] < '2018-01-13T16:07:18' /* @p4 */)
OR ([LPA_L1].[NextReviewDate] < '2018-01-19T16:07:18' /* @p5 */)) THEN 1
ELSE 0
END = 1 THEN 'wwarntwo' /* @p7 */
ELSE
CASE
WHEN CASE
WHEN (([LPA_L1].[NextReviewDate] < '2018-01-20T16:07:18' /* @p8 */)
OR ([LPA_L1].[NextReviewDate] < '2018-01-26T16:07:18' /* @p9 */)) THEN 1
ELSE 0
END = 1 THEN 'awarnone' /* @p11 */
ELSE '' /* @p13 */
END
END
END AS [Overdue],
[LPA_L1].[StaffMemberName32] AS [LastReviewedBy],
[LPA_L1].[LastReviewedDate]
FROM (SELECT [LPA_L4].[Default_] AS [Default],
[LPA_L4].[Item_Level_Num] AS [ItemLevelNum],
[LPA_L4].[Organisation_Code] AS [OrganisationCode],
[LPA_L4].[Org_Item_Description] AS [OrgItemDescription],
[LPA_L4].[Org_Item_ID] AS [OrgItemID],
[LPA_L4].[Org_Item_Level] AS [OrgItemLevel],
[LPA_L4].[Org_Item_Parent] AS [OrgItemParent],
[LPA_L4].[Org_Item_Sequence] AS [OrgItemSequence],
[LPA_L4].[Org_Item_Status] AS [OrgItemStatus],
[LPA_L4].[Party_ID] AS [PartyID],
1 /* @p15 */ AS [LPFA_41],
[LPA_L3].[Business_Unit_Affected_ID] AS [BusinessUnitAffectedID],
[LPA_L3].[Classification_ID] AS [ClassificationID],
[LPA_L3].[Date_Identified] AS [DateIdentified],
[LPA_L3].[Entered_By_ID] AS [EnteredByID],
[LPA_L3].[Entered_On] AS [EnteredOn],
[LPA_L3].[Feedback],
[LPA_L3].[Feedback_By_ID] AS [FeedbackByID],
[LPA_L3].[Feedback_Entered_On] AS [FeedbackEnteredOn],
[LPA_L3].[Hazard_Location_ID] AS [HazardLocationID],
[LPA_L3].[Hazard_No] AS [HazardNo],
[LPA_L3].[Hazard_No_Sortable] AS [HazardNoSortable],
[LPA_L3].[Hazard_Statement] AS [HazardStatement],
[LPA_L3].[Hazard_Status_ID] AS [HazardStatusID],
[LPA_L3].[Hazard_Title] AS [HazardTitle],
[LPA_L3].[Identified_By_ID] AS [IdentifiedByID],
[LPA_L3].[Last_Reviewed_By_ID] AS [LastReviewedByID],
[LPA_L3].[Last_Reviewed_Date] AS [LastReviewedDate],
[LPA_L3].[Next_Review_Date] AS [NextReviewDate],
[LPA_L3].[Potential_Current_ID] AS [PotentialCurrentID],
[LPA_L3].[Publish],
[LPA_L3].[WR_Number] AS [WRNumber],
[LPA_L3].[WR_Period] AS [WRPeriod],
[LPA_L3].[WR_Type] AS [WRType],
1 /* @p17 */ AS [LPFA_42],
[LPA_L5].[Computer_Username] AS [ComputerUsername],
[LPA_L5].[Created_On] AS [CreatedOn],
[LPA_L5].[Daily_Login_Fails] AS [DailyLoginFails],
[LPA_L5].[Department_ID] AS [DepartmentID],
[LPA_L5].[Email_Address] AS [EmailAddress],
[LPA_L5].[Enforce_Password_Expiration] AS [EnforcePasswordExpiration],
[LPA_L5].[Enforce_Password_Policy] AS [EnforcePasswordPolicy],
[LPA_L5].[Externally_Managed] AS [ExternallyManaged],
[LPA_L5].[External_StaffMember_Ref] AS [ExternalStaffMemberRef],
[LPA_L5].[Fax_Number] AS [FaxNumber],
[LPA_L5].[Last_LoggedOn] AS [LastLoggedOn],
[LPA_L5].[Last_Login_Fail_On] AS [LastLoginFailOn],
[LPA_L5].[Locked],
[LPA_L5].[Locked_On] AS [LockedOn],
[LPA_L5].[Party_ID] AS [PartyID0],
[LPA_L5].[Password],
[LPA_L5].[Password_Last_Changed_On] AS [PasswordLastChangedOn],
[LPA_L5].[Phone_Number] AS [PhoneNumber],
[LPA_L5].[Portal_Password_Hash] AS [PortalPasswordHash],
[LPA_L5].[Position],
[LPA_L5].[Require_Password_Change] AS [RequirePasswordChange],
[LPA_L5].[Staff_Member_ID] AS [StaffMemberID],
[LPA_L5].[Staff_Member_Name] AS [StaffMemberName],
[LPA_L5].[Status],
1 /* @p19 */ AS [LPFA_43],
[LPA_L6].[Adequate],
[LPA_L6].[Descriptor_Subject_ID] AS [DescriptorSubjectID],
[LPA_L6].[Group_Sequence] AS [GroupSequence],
[LPA_L6].[Item_ID] AS [ItemID],
[LPA_L6].[Item_Level] AS [ItemLevel],
[LPA_L6].[Item_Status] AS [ItemStatus],
[LPA_L6].[Item_Text] AS [ItemText],
[LPA_L6].[Parent_Item_ID] AS [ParentID],
1 /* @p21 */ AS [LPFA_44],
[LPA_L7].[Adequate] AS [Adequate1],
[LPA_L7].[Descriptor_Subject_ID] AS [DescriptorSubjectID2],
[LPA_L7].[Group_Sequence] AS [GroupSequence3],
[LPA_L7].[Item_ID] AS [ItemID4],
[LPA_L7].[Item_Level] AS [ItemLevel5],
[LPA_L7].[Item_Status] AS [ItemStatus6],
[LPA_L7].[Item_Text] AS [ItemText7],
[LPA_L7].[Parent_Item_ID] AS [ParentID8],
1 /* @p23 */ AS [LPFA_45],
[LPA_L8].[Hazard_Status] AS [HazardStatus],
[LPA_L8].[Hazard_Status_ID] AS [HazardStatusID9],
[LPA_L8].[Seq_No] AS [SeqNo],
1 /* @p25 */ AS [LPFA_46],
[LPA_L9].[Computer_Username] AS [ComputerUsername10],
[LPA_L9].[Created_On] AS [CreatedOn11],
[LPA_L9].[Daily_Login_Fails] AS [DailyLoginFails12],
[LPA_L9].[Department_ID] AS [DepartmentID13],
[LPA_L9].[Email_Address] AS [EmailAddress14],
[LPA_L9].[Enforce_Password_Expiration] AS [EnforcePasswordExpiration15],
[LPA_L9].[Enforce_Password_Policy] AS [EnforcePasswordPolicy16],
[LPA_L9].[Externally_Managed] AS [ExternallyManaged17],
[LPA_L9].[External_StaffMember_Ref] AS [ExternalStaffMemberRef18],
[LPA_L9].[Fax_Number] AS [FaxNumber19],
[LPA_L9].[Last_LoggedOn] AS [LastLoggedOn20],
[LPA_L9].[Last_Login_Fail_On] AS [LastLoginFailOn21],
[LPA_L9].[Locked] AS [Locked22],
[LPA_L9].[Locked_On] AS [LockedOn23],
[LPA_L9].[Party_ID] AS [PartyID24],
[LPA_L9].[Password] AS [Password25],
[LPA_L9].[Password_Last_Changed_On] AS [PasswordLastChangedOn26],
[LPA_L9].[Phone_Number] AS [PhoneNumber27],
[LPA_L9].[Portal_Password_Hash] AS [PortalPasswordHash28],
[LPA_L9].[Position] AS [Position29],
[LPA_L9].[Require_Password_Change] AS [RequirePasswordChange30],
[LPA_L9].[Staff_Member_ID] AS [StaffMemberID31],
[LPA_L9].[Staff_Member_Name] AS [StaffMemberName32],
[LPA_L9].[Status] AS [Status33],
1 /* @p27 */ AS [LPFA_47]
FROM ((((((([AQD].[rm_Hazard] [LPA_L3]
LEFT JOIN [AQD].[gn_Organisation_Structure_Port] [LPA_L4]
ON [LPA_L3].[Business_Unit_Affected_ID] = [LPA_L4].[Org_Item_ID])
LEFT JOIN [AQD].[gn_Staff_Member] [LPA_L5]
ON [LPA_L3].[Entered_By_ID] = [LPA_L5].[Staff_Member_ID])
LEFT JOIN [AQD].[rm_Hazard_Risk_Descriptors] [LPA_L6]
ON [LPA_L3].[Hazard_Location_ID] = [LPA_L6].[Item_ID])
LEFT JOIN [AQD].[rm_Hazard_Risk_Descriptors] [LPA_L7]
ON [LPA_L3].[Classification_ID] = [LPA_L7].[Item_ID])
LEFT JOIN [AQD].[rm_Hazard_Status] [LPA_L8]
ON [LPA_L3].[Hazard_Status_ID] = [LPA_L8].[Hazard_Status_ID])
LEFT JOIN [AQD].[gn_Staff_Member] [LPA_L9]
ON [LPA_L3].[Last_Reviewed_By_ID] = [LPA_L9].[Staff_Member_ID])
INNER JOIN [AQD].[gn_Staff_Member] [LPA_L10]
ON [LPA_L10].[Staff_Member_ID] = [LPA_L3].[Entered_By_ID])
WHERE (([LPA_L10].[Staff_Member_ID] = 136 /* @p28 */))) [LPA_L1]
It's twice joining on [Entered_By_ID] = [Staff_Member_ID] - one for filter and one for projection. Returns the right results in this instance but the duplicate join is not a pattern I would bet the house on.
Joined: 17-Aug-2003
Due to a fix we had to make (to v5.3.3) we'll generate the typedlist queries simpler now, without the required immediate projection with entities. This solves a problem with inheritance and makes the generated queries much simpler and thus faster. https://www.llblgen.com/tinyforum/GotoMessage.aspx?MessageID=140065&ThreadID=24601
So this idea won't make it, I'm afraid.
Joined: 25-Oct-2005
Otis wrote:
Due to a fix we had to make (to v5.3.3) we'll generate the typedlist queries simpler now, without the required immediate projection with entities.
So this idea won't make it, I'm afraid.
Is the other option of 'an optional parameter which is by default null and if not null, will be assigned to current0 instead of the default one.' still on the table?
Joined: 17-Aug-2003
There wasn't anything specific finalized in this discussion about that? If something's passed in, the whole join stack is then still performed, so you can only filter on the root entity, that's what you want?
Joined: 17-Aug-2003
Implemented in v5.4.
Normal:
// linq
[Test]
public void TypedListAsPocoLinqQueryTest4()
{
using(var adapter = new DataAccessAdapter())
{
var metaData = new LinqMetaData(adapter);
var q = from x in metaData.GetSingleEntityPocoLinqTypedList()
where x.CompanyName.StartsWith("S")
select x;
var results = ((ILLBLGenProQuery)q).Execute<List<SingleEntityPocoLinqRow>>();
Assert.AreEqual(7, results.Count);
}
}
// sql
SELECT [LPA_L1].[CustomerId],
[LPA_L1].[CompanyName],
[LPA_L1].[ContactName]
FROM (SELECT [LPLA_1].[CustomerID] AS [CustomerId],
[LPLA_1].[CompanyName],
[LPLA_1].[ContactName]
FROM [Northwind].[dbo].[Customers] [LPLA_1]) [LPA_L1]
WHERE (((([LPA_L1].[CompanyName] LIKE 'S%' /* @p1 */))))
New:
// linq
[Test]
public void TypedListAsPocoLinqQueryTest5()
{
using(var adapter = new DataAccessAdapter())
{
var metaData = new LinqMetaData(adapter);
var q = from x in metaData.GetSingleEntityPocoLinqTypedList(metaData.Customer.Where(c=>c.CompanyName.StartsWith("S")))
select x;
var results = ((ILLBLGenProQuery)q).Execute<List<SingleEntityPocoLinqRow>>();
Assert.AreEqual(7, results.Count);
}
}
// sql
SELECT [LPLA_1].[CustomerID] AS [CustomerId],
[LPLA_1].[CompanyName],
[LPLA_1].[ContactName]
FROM [Northwind].[dbo].[Customers] [LPLA_1]
WHERE ((((([LPLA_1].[CompanyName] LIKE 'S%' /* @p1 */)))))
As the predicate is now inside the projection, the query isn't wrapped and therefore easier to parse by the DB as well (doesn't matter much, but still).
The same will also be supported for the fetch methods for poco typed lists in QuerySpec.