- Home
- LLBLGen Pro
- LLBLGen Pro Runtime Framework
Dto persistence error on null related entity
Joined: 18-May-2011
I've created a derived dto model from entities with a m:0..1 relationship. The dto objects are set to retrieve the related entities when fetched/projected.
In the event of a null related entity (the related database record doesn't exist) - which is fine, and possible in my design, the persistence projector throws the following error. The error doesn't happen as long as there is a full related entity. Many times, there will not be one.
Object reference not set to an instance of an object.
Server stack trace: at System.ServiceModel.Channels.ServiceChannel.ThrowIfFaultUnderstood(Message reply, MessageFault fault, String action, MessageVersion version, FaultConverter faultConverter) at System.ServiceModel.Channels.ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc) at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout) at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation) at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)
Joined: 18-May-2011
Version 5.0 (5.0.4) RTM Build 17-Jun-2016
Using LLBLGen Framework.
The generated portion of the persistence project which causes this error is included below. In this case, if FailAction (a related entity) or SuccessAction is null, this will throw the exception.
private static System.Linq.Expressions.Expression<Func<Pco.Data.CallRouting.EntityClasses.ActionEntity, Pco.Data.CallRouting.Dto.DtoClasses.Action>> CreateProjectionFunc()
{
return p__0 => new Pco.Data.CallRouting.Dto.DtoClasses.Action()
{
ActionId = p__0.ActionId,
ActionName = p__0.ActionName,
ActionTypeId = p__0.ActionTypeId,
Created = p__0.Created,
CreatedBy = p__0.CreatedBy,
Description = p__0.Description,
FailAction = new Pco.Data.CallRouting.Dto.DtoClasses.ActionTypes.FailAction()
{
ActionId = p__0.FailAction.ActionId,
ActionName = p__0.FailAction.ActionName,
ActionTypeId = p__0.FailAction.ActionTypeId,
Created = p__0.FailAction.Created,
CreatedBy = p__0.FailAction.CreatedBy,
Description = p__0.FailAction.Description,
FailActionId = p__0.FailAction.FailActionId,
FunctionId = p__0.FailAction.FunctionId,
Modified = p__0.FailAction.Modified,
ModifiedBy = p__0.FailAction.ModifiedBy,
Precedence = p__0.FailAction.Precedence,
SecondsToFail = p__0.FailAction.SecondsToFail,
SuccessActionId = p__0.FailAction.SuccessActionId,
TimeConditionId = p__0.FailAction.TimeConditionId,
},
FailActionId = p__0.FailActionId,
FunctionId = p__0.FunctionId,
Modified = p__0.Modified,
ModifiedBy = p__0.ModifiedBy,
Precedence = p__0.Precedence,
SecondsToFail = p__0.SecondsToFail,
SuccessAction = new Pco.Data.CallRouting.Dto.DtoClasses.ActionTypes.SuccessAction()
{
ActionId = p__0.SuccessAction.ActionId,
ActionName = p__0.SuccessAction.ActionName,
ActionTypeId = p__0.SuccessAction.ActionTypeId,
Created = p__0.SuccessAction.Created,
CreatedBy = p__0.SuccessAction.CreatedBy,
Description = p__0.SuccessAction.Description,
FailActionId = p__0.SuccessAction.FailActionId,
FunctionId = p__0.SuccessAction.FunctionId,
Modified = p__0.SuccessAction.Modified,
ModifiedBy = p__0.SuccessAction.ModifiedBy,
Precedence = p__0.SuccessAction.Precedence,
SecondsToFail = p__0.SuccessAction.SecondsToFail,
SuccessActionId = p__0.SuccessAction.SuccessActionId,
TimeConditionId = p__0.SuccessAction.TimeConditionId,
},
SuccessActionId = p__0.SuccessActionId,
TimeConditionId = p__0.TimeConditionId,
};
}
Joined: 17-Aug-2003
I can't really reproduce it
[Test]
public void Product_FetchAllWithOptionalRelatedElement()
{
var results = GetIQueryable<ProductEntity>().ProjectToProduct().ToList();
Assert.AreEqual(77, results.Count);
foreach(var v in results)
{
Assert.IsNotNull(v.Category);
if(v.ProductId == 75)
{
Assert.IsNull(v.Category.CategoryName);
Assert.IsNull(v.Category.Description);
}
else
{
Assert.IsNotNull(v.Category.CategoryName);
Assert.IsNotNull(v.Category.Description);
}
}
}
// SQL:
SELECT [LPA_L1].[CategoryName],
[LPA_L1].[Description],
[LPA_L2].[Discontinued],
[LPA_L2].[ProductID] AS [ProductId],
[LPA_L2].[ProductName]
FROM ( [Northwind].[dbo].[Categories] [LPA_L1]
RIGHT JOIN [Northwind].[dbo].[Products] [LPA_L2]
ON [LPA_L1].[CategoryID] = [LPA_L2].[CategoryID])
// Projection lambda (generated)
private static System.Linq.Expressions.Expression<Func<NWMM.Adapter.EntityClasses.ProductEntity, NWMM.Dtos.DtoClasses.Product>> CreateProjectionFunc()
{
return p__0 => new NWMM.Dtos.DtoClasses.Product()
{
Category = new NWMM.Dtos.DtoClasses.ProductTypes.Category()
{
CategoryName = p__0.Category.CategoryName,
Description = p__0.Category.Description,
},
Discontinued = p__0.Discontinued,
ProductId = p__0.ProductId,
ProductName = p__0.ProductName,
};
}
Succeeds. 1 Product, with ID 75, has NULL for CategoryID, and therefore no related entity. This is on Northwind: Product m:0..1 Category.
As the stacktrace you have posted is incomplete it's not possible to say where the exception occurs. Please write a separate testcase to illustrate where exactly the null reference occurs.
The exception is impossible to occur in the generated lambda, as it's used to build a new one in-memory. It's first used to build a query, which results in a join due to the p__0.Category.... navigation, and it's then used to build a projector lambda (which is compiled and cached), which looks something like:
(values, indices) => new NWMM.Dtos.DtoClasses.Product()
{
Category = new NWMM.Dtos.DtoClasses.ProductTypes.Category()
{
CategoryName = values[indices[0]],
Description = values[indices[1]],
},
Discontinued = values[indices[2]],
ProductId = values[indices[3]],
ProductName = values[indices[4]],
};
or something like that, which means 'values' is the row read from the datareader, indices is the index array in that values array. For these queries it's likely even more optimized in that indices[n] is likely simply 'n', so no way to make this run into an NRE, it simply reads a value.
What is possible however is that you read a value from the nested element which is represented by null (in my test, this is v.Category). But as you haven't given that code, I can only guess.
Joined: 18-May-2011
So it seems that my issue may be related to the fact that the related entity, is actually a parent-child relationship with the same table.
I was able to reproduce the issue in a simpler project. Created a Sql Server table MyAction:
CREATE TABLE [dbo].[MyAction](
[ActionID] [int] IDENTITY(1,1) NOT NULL,
[ActionName] [varchar](50) NOT NULL,
[SuccessActionID] [int] NULL,
[FailActionID] [int] NULL,
CONSTRAINT [PK_MyAction_1] PRIMARY KEY CLUSTERED ([ActionID] ASC)
)
GO
ALTER TABLE [dbo].[MyAction] WITH CHECK ADD CONSTRAINT [FK_MyAction_FailAction] FOREIGN KEY([FailActionID])
REFERENCES [dbo].[MyAction] ([ActionID])
GO
ALTER TABLE [dbo].[MyAction] CHECK CONSTRAINT [FK_MyAction_FailAction]
GO
ALTER TABLE [dbo].[MyAction] WITH CHECK ADD CONSTRAINT [FK_MyAction_SuccessAction] FOREIGN KEY([SuccessActionID])
REFERENCES [dbo].[MyAction] ([ActionID])
GO
ALTER TABLE [dbo].[MyAction] CHECK CONSTRAINT [FK_MyAction_SuccessAction]
GO
I created three records: ActionID, ActionName, SuccessActionID, FailActionID 1, First Action, 2, 3 2, Second Action, 3, NULL 3, Third Action, NULL, NULL
Then created and generated the code using LLBLGen Framework, .Net 4.6. The dto object includes the FailAction and SuccessAction objects (one level).
A new Wcf project using the objects and implementing a service layer has a method GetData:
public MyAction GetData(int id)
{
MyAction ret = new MyAction();
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
MyActionEntity action = new MyActionEntity(id);
PrefetchPath2 path = new PrefetchPath2(EntityType.MyActionEntity);
path.Add(MyActionEntity.PrefetchPathFailAction);
path.Add(MyActionEntity.PrefetchPathSuccessAction);
adapter.FetchEntity(action, path);
ret = MyActionPersistence.ProjectToMyAction(action);
}
return ret;
}
This code will work fine for id=1. The error will throw for id=2 on this line:
ret = MyActionPersistence.ProjectToMyAction(action);
Stack trace: Object reference not set to an instance of an object.
Server stack trace: at System.ServiceModel.Channels.ServiceChannel.ThrowIfFaultUnderstood(Message reply, MessageFault fault, String action, MessageVersion version, FaultConverter faultConverter) at System.ServiceModel.Channels.ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc) at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout) at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation) at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)
Exception rethrown at [0]: at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg) at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type) at IService1.GetData(Int32 id) at Service1Client.GetData(Int32 id)
Joined: 17-Aug-2003
Ah, I see what's going wrong:
adapter.FetchEntity(action, path);
ret = MyActionPersistence.ProjectToMyAction(action);
You use the projector method on an in-memory graph. While this is supported, it uses the same projector lambda as the one which is used in DB queries.
I think you ran into a design flaw in our code: the projector for in-memory projections is only usable on a graph with non-null references.
There's a better way to fetch the DTOs however, which also frees you from the problem: use it with an IQueryable:
public MyAction GetData(int id)
{
MyAction ret = null;
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
var metaData = new LinqMetaData(adapter);
var q = metaData.MyAction.Where(a=>a.Id==id);
ret = q.ProjectToMyAction().FirstOrDefault();
}
return ret;
}
This runs the entire projection in the database in 1 query and doesn't first materialize the set into a graph of entities (which runs 1 query per graph node, so in your case 3 queries).
As it's meant to be run in the DB, there are no checks whether an element is null, as the projector is used to produce the query and the resultset is a flat list which is never going to need the null check. Your graph has nulled element references and therefore will run into problems here.
We'll look into correcting this flaw, in v5.0.5 or later. We can't just emit null checks as the projector is also used for creating a query and that query doesn't need the null checks so we have to verify whether we can keep the single lambda or have to use 2.
Joined: 18-May-2011
Very clean, thanks. Got it working this way.
Does this only work with Linq? I much prefer QuerySpec to Linq as I think is is much easier to code and incredibly easy to read. Can this same sort of projection be done using QuerySpec? The example is a fetch by ID, but what if the fetch was more complex?
Something like this:
var qf = new QueryFactory();
var q = qf.DialedNumberAction
.From(QueryTarget.InnerJoin(DialedNumberActionEntity.Relations.ActionEntityUsingActionId)
.InnerJoin(ActionEntity.Relations.TimeConditionEntityUsingTimeConditionId)
.InnerJoin(DialedNumberActionEntity.Relations.DialedNumberEntityUsingDialedNumberId))
.Where(TimeConditionFields.FromTime <= timereference.TimeOfDay)
.AndWhere(TimeConditionFields.ToTime >= timereference.TimeOfDay)
.AndWhere(TimeConditionFields.ActiveDate <= timereference)
.AndWhere(TimeConditionFields.ExpireDate >= timereference)
.AndWhere(DialedNumberFields.DialedNum == "+1" + did)
.WithPath(DialedNumberActionEntity.PrefetchPathAction,
DialedNumberActionEntity.PrefetchPathDialedNumber
)
.OrderBy(ActionFields.Precedence.Ascending());
Joined: 17-Aug-2003
mjking wrote:
Very clean, thanks. Got it working this way.
Does this only work with Linq? I much prefer QuerySpec to Linq as I think is is much easier to code and incredibly easy to read. Can this same sort of projection be done using QuerySpec? The example is a fetch by ID, but what if the fetch was more complex?
Something like this:
var qf = new QueryFactory(); var q = qf.DialedNumberAction .From(QueryTarget.InnerJoin(DialedNumberActionEntity.Relations.ActionEntityUsingActionId) .InnerJoin(ActionEntity.Relations.TimeConditionEntityUsingTimeConditionId) .InnerJoin(DialedNumberActionEntity.Relations.DialedNumberEntityUsingDialedNumberId)) .Where(TimeConditionFields.FromTime <= timereference.TimeOfDay) .AndWhere(TimeConditionFields.ToTime >= timereference.TimeOfDay) .AndWhere(TimeConditionFields.ActiveDate <= timereference) .AndWhere(TimeConditionFields.ExpireDate >= timereference) .AndWhere(DialedNumberFields.DialedNum == "+1" + did) .WithPath(DialedNumberActionEntity.PrefetchPathAction, DialedNumberActionEntity.PrefetchPathDialedNumber ) .OrderBy(ActionFields.Precedence.Ascending());
At the moment it works with Linq only, but we're planning to add QuerySpec support in a future version (might not make it to 5.1 though, but after that).