Dto persistence error on null related entity

Posts   
 
    
mjking
User
Posts: 5
Joined: 18-May-2011
# Posted on: 27-Jun-2016 23:40:34   

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)

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 28-Jun-2016 08:11:20   

Please post your LLBLGen version and build, and the code that triggers such exception.

David Elizondo | LLBLGen Support Team
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39615
Joined: 17-Aug-2003
# Posted on: 28-Jun-2016 09:18:51   

Additionally, which ORM you're using is also likely to be important, so we need to know that too. This is when loading the data, I presume?

Frans Bouma | Lead developer LLBLGen Pro
mjking
User
Posts: 5
Joined: 18-May-2011
# Posted on: 28-Jun-2016 14:41:08   

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,
    };
}

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39615
Joined: 17-Aug-2003
# Posted on: 28-Jun-2016 15:10:26   

Hmm, we will look into it.

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39615
Joined: 17-Aug-2003
# Posted on: 28-Jun-2016 15:39:26   

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.

Frans Bouma | Lead developer LLBLGen Pro
mjking
User
Posts: 5
Joined: 18-May-2011
# Posted on: 28-Jun-2016 21:27:34   

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)

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39615
Joined: 17-Aug-2003
# Posted on: 29-Jun-2016 10:40:45   

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.

Frans Bouma | Lead developer LLBLGen Pro
mjking
User
Posts: 5
Joined: 18-May-2011
# Posted on: 30-Jun-2016 02:03:14   

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());

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39615
Joined: 17-Aug-2003
# Posted on: 30-Jun-2016 10:17:45   

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).

Frans Bouma | Lead developer LLBLGen Pro