NullReferenceException in Derived Model when the Single-element reference is null

Posts   
 
    
Posts: 37
Joined: 09-Nov-2016
# Posted on: 12-Apr-2018 13:43:01   

Hi,

I am using LLBLGen 5.4 Beta and LLBLGen Runtime Pro Framework in a .NET 2.0 Core Class Library. If I have the below (pseudo) model where Engine is optional in the Car (undergoing repairs or something)


  public class Car
  {
        public int Id { get; set; }

        public string Model { get; set; }

        //EngineFK
        public int? EngineId { get; set; }
  }

  public class Engine
  {
        public int Id { get; set; }

        public string Name { get; set; }
  }

In the Designer the Engine is marked as Optional. When I create a Derived Model (Dto) from the Car, I select the Engine to get the Name of the Engine along with everything else. The Engine in the designer is set to "Single-element reference" in the "Derived Element Sub-Element Selection" and "Field, Single Element / Embedded Element" in the "Derived Element Shape Editor".

This works great if Engine has a value, but it throws a NullReferenceException in "carEntity.ProjectToCarDto()" if Engine does not have a value.

Am I missing some configuration to simply return null if a sub-element is null? Since it is marked as optional, I can't use it in the Dto. The only workaround I have found is to remove Engine from the CarDto, create a separate EngineDto, check if Car has an engine and if it does, get the EngineDto and attach this to the CarDto.

Best regards Andreas

Walaa avatar
Walaa
Support Team
Posts: 14946
Joined: 21-Aug-2005
# Posted on: 12-Apr-2018 21:02:39   

I can't reproduce it. Null fields are returned inside the derived model (DTO) for the null referenced entity. No exception is thrown.

Posts: 37
Joined: 09-Nov-2016
# Posted on: 12-Apr-2018 22:39:23   

Hmm... Then I am probably missing something obvious. I have attached a quick example (it's late here, so its not pretty).

  • The Solution and test program is in the Code folder.
  • The SQL needed to create the database is in the Database folder.
  • The LLBLGen project is in the root.

It basically does this:


var path = new PrefetchPath2(EntityType.CarEntity)
{
    CarEntity.PrefetchPathEngine
};

var carWithEngine = metaData.Car.WithPath(path).First(x => x.Type == "WithEngine");
var carWithoutEngine = metaData.Car.WithPath(path).First(x => x.Type == "WithoutEngine");

//Works
var carWithEngineDto = carWithEngine.ProjectToAutomobileCar();

//Fails
var carWithoutEngineDto = carWithoutEngine.ProjectToAutomobileCar();

Attachments
Filename File size Added on Approval
LLBLGenDTOTest.zip 808,534 12-Apr-2018 22:39.35 Approved
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39588
Joined: 17-Aug-2003
# Posted on: 13-Apr-2018 09:41:14   

If you first project the source query to entities in-memory, and then use the DTO projection then it can go wrong as the projection method doesn't check for null values for related objects. If you use the projection method directly in the DB query, you don't get this issue as the source query isn't projected to entities first.

For DTOs you don't need to specify the prefetch path either. Just fetch the root entity and specify filters on that query, then use the projection method to project the set to DTOs. It will automatically fetch related data (in your case 'Engine') if needed (which is dictated by the embedded derived elements inside the code DTO type).

Frans Bouma | Lead developer LLBLGen Pro
Posts: 37
Joined: 09-Nov-2016
# Posted on: 13-Apr-2018 09:53:30   

Ahhh... In my project I am experiencing this problem before I actually save the entity to the database. I need to calculate the hash of an entity to see if it already exists in the database before saving it.

I have a working work-around where I convert the entity to ExpandoObject and manually add the properties. Basically something like this (just in case other people need it):


if (entity.Engine != null)
{
    engine = Converter.ConvertToExpando(entity.Engine.ProjectToEngineDto());
}

public static ExpandoObject ConvertToExpando(object input)
{
    var inputAsJson = JsonConvert.SerializeObject(input);

    var converter = new ExpandoObjectConverter();
    return JsonConvert.DeserializeObject<ExpandoObject>(inputAsJson, converter);
}

EDIT: Clarification.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39588
Joined: 17-Aug-2003
# Posted on: 13-Apr-2018 16:05:37   

Hmm, isn't it preferable to simply try to fetch the entity, update it if necessary (or augment the new entity) and then save it? Checking it will never be 100% solid (between checking and saving a new entity can be inserted) and e.g. using a PK check might be sufficient?

If the save fails because the insert fails as there's already such an entity, you can update it by fetching it. Of course, inserts never fail if the pk is artificial and there are no unique constraints...

Frans Bouma | Lead developer LLBLGen Pro
Posts: 37
Joined: 09-Nov-2016
# Posted on: 13-Apr-2018 20:03:50   

I am not sure the problem relates to LLBLGen anymore (so don't have to spend time on it), but here is the story:

The origin of the problem is mapping between two data structures. One is from an existing (and old) database and the other is a new database.

In the old datamodel a Car (if we continue with this metaphor) is saved in a somewhat flat structure. This means that, for example, the Material and the Car Designer is saved as a string.

In the new model, the material is a simple basedata class (Id and MaterialName) with a 1-Many relationship to the Car. The Car Designer is a more complex class consisting of both basedata classes (for example City and Country) and "normal" classes as Address. The Car Designer has a Many-To-Many relationship with Car.

When I map the data from the old to the new, I need to check the database for existing Cars, Car Designers and all associated basedata (Material, City and Country).

The basedata is simple (something like: if basedata.Name == "new basedata" then either create a new one or return the existing one), however Cars and Car Designer is more complex.

The Linq queries to test for a, for example, Car Designer got to complex and had to take into account that some basedata does not exist. The code below is taken from a real method:


public static LocationEntity AddOrGetLocationEntity(string address, string zipCode, string city, double? latitude, double? longitude, double? heading, string country, int? radius, string description, string collection)
{
    using (DataAccessAdapter adapter = new DataAccessAdapter())
    {
        var data = new LinqMetaData(adapter);

        var locationEntity = data.Location.SingleOrDefault(x => string.Equals(x.Address.Trim(), address.Trim(), StringComparison.OrdinalIgnoreCase)
            && string.Equals(x.City.Name.Trim(), city.Trim(), StringComparison.OrdinalIgnoreCase)
            && string.Equals(x.ZipCode.Trim(), zipCode.Trim(), StringComparison.OrdinalIgnoreCase)
            && x.Latitude == latitude
            && x.Longitude == longitude
            && string.Equals(x.Country.Name.Trim(), country.Trim(), StringComparison.OrdinalIgnoreCase)
            && x.Radius == radius
            && x.Heading == heading
            && string.Equals(x.Description.Trim(), description.Trim(), StringComparison.OrdinalIgnoreCase)
            && string.Equals(x.Collection.Name.Trim(), collection.Trim(), StringComparison.OrdinalIgnoreCase)
        );
        
        ...
    }
}

This obviously fails if the some of the basedata is null and adding checks for that seemed to make the whole thing even more complex. This is when I thought about creating a Hash of the data and comparing that instead and hence the original question simple_smile

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 14-Apr-2018 09:32:31   

Thanks for clarifying the scenario and for the feedback. As I understood, the problem was identified and it's not at LLBLGen side, Right?

David Elizondo | LLBLGen Support Team
Posts: 37
Joined: 09-Nov-2016
# Posted on: 16-Apr-2018 09:54:40   

Correct simple_smile

Thank you for the help.