2

When I select from a parent object in Linq to SQL, I get all records in the parent table. However, when I also select a navigation property (from an extension table with a 1:1 relationship and a shared primary key), I get only records that have that navigation property populated. How can I use the navigation property to get all records from the parent, regardless of whether the child is populated? (i.e. a left instead of an inner join)? Can Linq to SQL navigation properties do this, or is a manual join the only option? One of the proposed answers on this post says it should be possible, and this post says that Entity Framework would handle it, but I can't get it to work with Linq to SQL.

The queries:

        TestDataContext repository = new TestDataContext();

        // No navigation property selected, returns all records from Persons
        var result = repository.Persons.Select(x => new { x.PersonID, x.Name });
        // Navigation property selected, returns only persons having a record in PersonExtensions table
        var result1 = repository.Persons.Select(x => new { x.PersonID, x.Name, x.Extension.Height });

Here are the domain classes:

[Table(Name = "Persons")]
public class Person
{
    private System.Data.Linq.EntityRef<PersonExtension> _extension = new System.Data.Linq.EntityRef<PersonExtension>();

    [Association(Name = "Persons_PersonExtensions", Storage = "_extension", ThisKey = "PersonID", OtherKey = "PersonID", IsForeignKey = true)]
    public PersonExtension Extension
    {
        get
        {
            return _extension.Entity;
        }
        set
        {
            _extension.Entity = value;
        }
    }

    [Column(Name = "PersonID", DbType = "int NOT NULL IDENTITY", IsPrimaryKey = true, IsDbGenerated = true, CanBeNull = false, UpdateCheck = System.Data.Linq.Mapping.UpdateCheck.Never)]
    public int PersonID { get; set; }

    [Column(Name = "Name")]
    public string Name { get; set; }

    [Column(Name = "Gender")]
    public string Gender { get; set; }
}

[Table(Name = "PersonExtensions")]
public class PersonExtension
{
    private System.Data.Linq.EntityRef<Person> _person = new System.Data.Linq.EntityRef<Person>();

    [Association(Name = "Persons_PersonExtensions", Storage = "_person", ThisKey = "PersonID", OtherKey = "PersonID", IsForeignKey = true)]
    public Person Person
    {
        get
        {
            return _person.Entity;
        }
        set
        {
            _person.Entity = value;
        }
    }

    [Column(Name = "PersonID", DbType = "int NOT NULL", IsPrimaryKey = true, CanBeNull = false, UpdateCheck = System.Data.Linq.Mapping.UpdateCheck.Never)]
    public int PersonID { get; set; }

    [Column(Name = "Height ")]
    public int? Height { get; set; }
}

And the datacontext:

[Database]
public class TestDataContext:DataContext
{
    public TestDataContext() : base(@"Data Source=localhost; Initial Catalog = Test; Integrated Security = True")
    {
        this.CommandTimeout = 600;
    }

    public Table<Person> Persons;
    public Table<PersonExtension> PersonExtensions;
}

Here are the table definitions:

CREATE TABLE [dbo].[Persons](
    [PersonID] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](50) NULL,
    [Gender] [nchar](10) NULL,
 CONSTRAINT [PK_Persons] PRIMARY KEY CLUSTERED 
(
    [PersonID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]


CREATE TABLE [dbo].[PersonExtensions](
    [PersonID] [int] NOT NULL,
    [Height] [int] NULL,
 CONSTRAINT [PK_PersonExtensions] PRIMARY KEY CLUSTERED 
(
    [PersonID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[PersonExtensions]  WITH CHECK ADD  CONSTRAINT [FK_PersonExtensions_Persons] FOREIGN KEY([PersonID])
REFERENCES [dbo].[Persons] ([PersonID])
GO

ALTER TABLE [dbo].[PersonExtensions] CHECK CONSTRAINT [FK_PersonExtensions_Persons]
GO

Any help will be very much appreciated!!

Note: question above has been modified slightly from the original to make it clearer that the goal is to use navigation properties.

Community
  • 1
  • 1
user756366
  • 467
  • 6
  • 24

2 Answers2

1

Replace zzz with Height's type.

var result1 = repository.Persons.Select(x =>
    new { x.PersonID, x.Name, x.Extension == null ? (zzz?)null : x.Extension.Height });

Update

Based on LINQ to SQL Left Outer Join:

var query = from person in repository.Persons
            join extension in repository.Extensions
                on person.ExtensionId equals extension.ExtensionId into g
            from x in g.DefaultIfEmpty() // Creates a LEFT OUTER JOIN.
            select new
            {
                person.PersonID,
                person.Name,
                Height = x == null ? (int?) : x.Height
            };

var result = query.ToArray();
Community
  • 1
  • 1
AgentFire
  • 8,944
  • 8
  • 43
  • 90
  • Hmm. I just tried that - changed the query line to the following (named the Height variable for the anonymous type to work), but I still get just the record that exists in PersonExtensions in the result set: var result1 = repository.Persons.Select(x => new { x.PersonID, x.Name, Height=x.Extension == null ? (int?) null : x.Extension.Height }); – user756366 Dec 15 '16 at 17:25
  • Unfortunately, still just the one record that exists in both tables is coming back. – user756366 Dec 15 '16 at 22:34
  • @user756366 there you go. – AgentFire Dec 15 '16 at 23:09
  • Thanks! That does work. Unfortunately though, I think I do need to leave the question open still... because what I'm really looking and hoping for is a way to use navigation properties and have linq auto-generate the left join. This solution instead ignores the navigation property and manually creates the join, which could work in many situations but will be tough to use with Dynamic SQL and user generated queries. I very much appreciate your help though! Thank you very much! Sorry not to be able to mark as answer! – user756366 Dec 16 '16 at 18:54
1

It appears that left joins in Linq to SQL navigation properties are possible, but the objects can't be set up the way I set mine up. Instead, the id of the "extension" table must be different from the id of the "main" table, and the "main" table must contain a nullable (e.g. int?) reference to the id of the extension table. It's the fact that the variable is nullable that tells Linq to SQL to create a left join instead of an inner join. Of course if I try to use the same key in both tables, that isn't possible, because the id of the main table has to be a non-nullable field in that table.

I figured this out based on this post.

Unfortunately, this is also messy, but I'm pretty sure at this point that this is the only option if I want calls to the navigation properties to generate a left join.

user756366
  • 467
  • 6
  • 24
  • You had NON-nullable ID? omg no wonder it didn't work for you. I don't get at all why you can have NULLABLE logical reference to another table, but still non-nullable ID field. That makes zero sense and indicate to a very huge design problem(s). – AgentFire Dec 16 '16 at 20:51
  • We want to have that because we have a main table that has existed for a long time and we want to add an extension table with more fields, custom per customer, to allow them to add fields they need. We are concerned about enforcing a rule that there HAS to be a record in that table for every record in the original table, because it's a complex existing system and, while we do plan to catch all places records are inserted, we are not 100% sure we'll catch all. We'd like the changes to not risk losing records in existing reports/queries. – user756366 Dec 20 '16 at 16:29