2

I am struggling to understand how to construct my SQL query using Entity Framework 6 and SQL Server in my ASP.NET Web API v2 project.

My table structure: the Computers table is my root table for all computers that have been inventoried by my software. It contains the column ComputerID which is used as a foreign key by other tables.

I can retrieve a computer including all information I have on it by using the query below:

Computer computer = ComputersDbContext.Computers
                   ... more includes....
                    .Include("Computer_Win_Installed_Software")
                   ... more includes....
                    .Where(a => a.ComputerId == computerId)
                       .First(t => t.TenantId == tenantId);

As you can see, I have another table called Computer_Win_Installed_Software which stores all software installed on a system. It looks like this:

IdentityKey | ComputerID (FK) | Softwarename     | EntryTimestamp
------------+-----------------+------------------+--------------
          1 |               1 | Some Software    | 1547241345 
          2 |               1 | Another Software | 1547241345 

EntryTimestamp is a Unix timestamp which is unique to every inventory run and is the same for all software discovered on that run. Now if the system gets inventoried again, the table will look like this:

IdentityKey | ComputerID (FK) | Softwarename       | EntryTimestamp
------------+-----------------+--------------------+---------------
          1 |               1 | Some Software      |     1547241345 
          2 |               1 | Another Software   |     1547241345 
          3 |               1 | Some Software      |     1886454564 
          4 |               1 | Another Software   |     1886454564 
          5 |               1 | Even More Software |     1886454564 

I want to keep historic data so I need to keep the old entries.

My problem is that my EF query from above will return ALL of these entries in the resulting object.

How do I change my query so that it only returns:

IdentityKey | ComputerID (FK) | Softwarename       | EntryTimestamp
------------+-----------------+--------------------+---------------
          3 |               1 | Some Software      |     1886454564 
          4 |               1 | Another Software   |     1886454564 
          5 |               1 | Even More Software |     1886454564 

I thought about using 2 queries:

  • First query: I check for the max value of EntryTimestamp
  • Second query: something like this:

    Computer computer = ComputersDbContext.Computers
                            .Include("Computer_Win_Installed_Software")      
                            .Where(a => a.ComputerId == computerId)
                            .Where(b => b.Computer_Win_Installed_Software.EntryTimestamp == EntryTimestamp)
                            .First(t => t.TenantId == tenantId);
    

But Intellisense immediately screams at me :D

I also thought about only selecting the MAX() of the column EntryTimestamp; but I cannot even use b.Computer_Win_Installed_Software.EntryTimestamp in the query code.

When I write: .Where(b => b.Computer_Win_Installed_Software, it doesn't list any of the columns as available options.

I think this is because the Computer_Win_Installed_Software class in EF is of type ICollection<Computer_Win_Installed_Software>. It is the same problem with other tables which all have a 1 to many relationship to the Computers table. Another table has a 1 to 1 relationship to the Computers table and there I can select all columns.

I'm super confused.

And yes I did google but I could not find anything that helped me out. What's the right way to go here?

Edit: Added Models

DBContext:

public class DbEntities : DbContext
{
    public virtual DbSet<Agent> Agents { get; set; }     
    public virtual DbSet<Computer_Win_Installed_Software> ComputerWinInstalledSoftware { get; set; }      
    public DbSet<Computer> Computers { get; set; }
}

EF Model for the Computer:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated from a template.
//
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace ProjectName
{
    using System;
    using System.Collections.Generic;

    public partial class Computer
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public Computer()
        {
            this.Computer_Win_Installed_Software = new HashSet<Computer_Win_Installed_Software>();            
        }

        public int ComputerId { get; set; }
        public int TenantId { get; set; }
        public virtual Agent Agent { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<Computer_Win_Installed_Software> Computer_Win_Installed_Software { get; set; }

    }
}

EF Model for Computer_Win_Installed_Software:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated from a template.
//
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace KysoWebApi
{
    using System;
    using System.Collections.Generic;

    public partial class Computer_Win_Installed_Software
    {
        public int ComputerId { get; set; }
        public string SoftwareName { get; set; }
        public Nullable<double> SoftwareVersion { get; set; }
        public Nullable<bool> IsServerSoftware { get; set; }
        public Nullable<int> SoftwareVendorId { get; set; }
        public string SoftwareIs32or64bits { get; set; }
        public int ComputerWinInstalledSoftwareEntryId { get; set; }
        public string EntryTimestamp { get; set; }

        public virtual Computer Computer { get; set; }
    }
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
soomon
  • 396
  • 2
  • 18
  • You are failing because you are not matching the type of the columns, What is the type of EntryTimestamp? Is it the same type as being used in the database? – jdweng Oct 22 '18 at 19:30
  • In the database it is: varchar(10) and in my code it is a string. But I cannot even select b.Computer_Win_Installed_Software.EntryTimestamp. It seems to be the same with all tables that are modeled as ICollection. These have a 1 to many relation to the computers table – soomon Oct 22 '18 at 19:36
  • Can you provide your model entities? – Mauricio Atanache Oct 22 '18 at 19:42
  • 1
    BTW, you can easily do it on the server side: `SELECT identitykey, computerid, softwarename, entrytimestamp FROM computer_win_installed_software WHERE entrytimestamp = (SELECT MAX(entrytimestamp) FROM computer_win_installed_software) ` – Miamy Oct 22 '18 at 19:52
  • @Mauricio Atanache: Added the EF models. I hope you meant that ones. If you meant something else, please clarify and I will happily provide them – soomon Oct 22 '18 at 19:52
  • @Miamy: Thank you! I could certainly do that, but it would require me to run 2 queries. I am quit sure that I will run into this problem in multiple places :/ Do you know a solution that I can integrate in my current query? – soomon Oct 22 '18 at 19:58
  • 1
    I don't see the `TenantId` field anywhere. – JuanR Oct 22 '18 at 20:09
  • @JuanR: Damn you are good! I'm sorry I removed references to other tables from models and code to improve readability and did remove it as well. I have added it again. Every computer has a link to a tenant, see the computer model. – soomon Oct 22 '18 at 20:17
  • It looks like you have three Databases (they are dbSet, not dbTable)1) Agents 2) ComputerWinInstalledSoftware 3) Computers Each database will have DataTables. Each Table will be a List object. You are trying to enumerate through a Database Computers which is not a List(). You need to find the name of the table inside Computer Database that has the info. – jdweng Oct 22 '18 at 21:06
  • What result do you want in the end? If you are looking for `ComputerWinInstalledSoftware` rows, why not query that? – NetMage Oct 22 '18 at 23:52
  • @jdweng: No I only have 1 single database. DbTable doe snot seem to exist, according to my Visual Studio IntelliSense. Can you please show me a link to the documentation? I couldn't find any. – soomon Oct 23 '18 at 05:07
  • @NetMage: I can get all rows from the `ComputerWinInstalledSoftware` table, but I want to filter them inside of the query as part of the computer query so that I do not query the database multiple times – soomon Oct 23 '18 at 05:08
  • Can you use SQL Server Management Studio to verify the database(s), tables, and Columns. The structure in Net may not match the database. Your config file may need updating. – jdweng Oct 23 '18 at 09:03
  • "Intellisense immediately screams at me". It's better to tell what it screams. I think you need `Where(b => b.Computer_Win_Installed_Software.Any(wis => wis.EntryTimestamp == EntryTimestamp))` – Gert Arnold Oct 23 '18 at 11:16
  • @GertArnold: Thanks for getting in touch with me! I am sad to report that your solution does not work. The result is: If there is no row with the value of `EntryId` -> no result. If there is one row with the correct value of `EntryId` -> returning all rows with the matching `ComputerId` , no matter of `EntryId` is correct on all of these rows. It does not check each row for the correct `EntryId`. – soomon Oct 23 '18 at 14:38
  • Replace `Any` by `All`. – Gert Arnold Oct 23 '18 at 14:40
  • Sadly, when I do that, I get a `Sequence contains no element Exception` exception – soomon Oct 24 '18 at 06:09

4 Answers4

1

So, for a given Computer, the ICollection<Computer_Win_Installed_Software> Computer_Win_Installed_Software is always going to include all related rows with all EntryTimestampts. You can't (easily) filter the entities included.

So, instead, just return the filtered entities:

var computer = ComputersDbContext.Computers
                .Where(a => a.ComputerId == computerId)
                .First(t => t.TenantId == tenantId);
var cwis = computer
            .Computer_Win_Installed_Software
                .Where(b => b.EntryTimestamp == EntryTimestamp);

Of course, I'm not sure this is what you actually want to do, you may have an XY Problem.

Can you try not including the related objects, but just querying against them?

var computer = ComputersDbContext.Computers
                .Where(a => a.ComputerId == computerId)
                .First(t => t.TenantId == tenantId);
                .Where(b => b.Computer_Win_Installed_Software.EntryTimestamp == EntryTimestamp);
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • Damn, that means if the table `Computer_Win_Installed_Software`contains 1000 Rows, I will always have to query all of them and filter afterwards? – soomon Oct 23 '18 at 05:15
  • Or don't use `Include` and retrieve them later. Your database and your entities seem to be at odds - I don't use EF but I would guess you should be migrating old entries to a history table so your entity expects all current rows to be joined. With some EF model changes, it might be possible to create an EF property that is just for current rows and automatically modifies the join for the latest. Apparently the EF method normally would be two queries, using the `Query` and `Load` methods to manually load filtered objects. – NetMage Oct 23 '18 at 17:36
  • I added a possibility where instead of including them, you just query against the relation. – NetMage Oct 23 '18 at 17:43
0

Please try this :

            var computer = db.ComputerWinInstalledSoftware
                .Include("Computer")
                .First(c => c.Computer.ComputerId == computerId 
                && c.Computer.TenantId == tenantId 
                && c.EntryTimestamp == EntryTimestamp ).Computer;
Mauricio Atanache
  • 2,424
  • 1
  • 13
  • 18
  • That is what I tried. The error is: Error CS1061 'ICollection' does not contain a definition for 'EntryTimestamp' and no accessible extension method 'EntryTimestamp' accepting a first argument of type 'ICollection' could be found (are you missing a using directive or an assembly reference?) – soomon Oct 22 '18 at 20:04
  • Just to confirm, this is different that the code you posted on line "Include", did you tried the change? – Mauricio Atanache Oct 23 '18 at 12:43
  • Have you noticed you have different namespaces in your entities? Is this on purpose? – Mauricio Atanache Oct 24 '18 at 12:33
0

Try this:

Computer computer = context.Computers
   .Include(c => c.Computer_Win_Installed_Software.Join(
       c.Computer_Win_Installed_Software.Where(a => a.ComputerId == computerId && a.Computer.TenantId == tenantId)
       .GroupBy(s => s.Softwarename)
       .Select(g => new { SoftwareName = g.Key, MaxDate = g.Max(r => r.EntryTimestamp) })
       ,o => o.EntryTimestamp
       ,i => i.MaxDate
       , (o,i) => o))
   .First(a => a.ComputerId == computerId && a.TenantId == tenantId);

I don't see the TenantId column anywhere on your Entity definition so just make sure to put the correct field in there.

What this is doing is joining to the records by max date.

UPDATE: I made a small correction. It should be grouping by Softwarename. Change the grouping to your liking so it correctly identifies each piece of software uniquely.

JuanR
  • 7,405
  • 1
  • 19
  • 30
  • I am sorry to say that this doesn't work. Everything in within .Include() is marked as an error: Error CS1660 Cannot convert lambda expression to type 'string' because it is not a delegate type. – soomon Oct 22 '18 at 20:31
  • @soomon: I took another look at your model and it looks like the `EntryTimeStamp` is a `string` (although it looks like a number). Try changing the `Max` statement to `r.EntryTimestamp.ToString()`. – JuanR Oct 23 '18 at 01:21
  • Thanks again. Yes it is a string. Sadly the error still exists. I think the error is not with your code but with the fact, that a lamda expression does not seem to be allowed within `Include()` – soomon Oct 23 '18 at 05:13
  • Alright I needed `using System.Data.Entity` namespace to remove the error. Also you are right about `EntryTimestamp` being a string. I changed it to an integer. The code still gives me an error `System.ArgumentException: The Include path expression must refer to a navigation property defined on the type. Use dotted paths for reference navigation properties and the Select operator for collection navigation properties. Parameter name: path at System.Data.Entity.QueryableExtensions.Include[T,TProperty](IQueryable\`1 source, Expression\`1 path)` – soomon Oct 23 '18 at 07:22
  • I tried: `.Include(c => c.Computer_Win_Installed_Software.Where(d => d.EntryTimestamp == 1540275242))` to conduct a simple test. It compiles but gives the same error. When I press the `.`after `d` it actually shows me the properties of `Computer_Win_Installed_Software` . So it knows that I want to use them, but the error stays the same – soomon Oct 23 '18 at 07:29
0

Alright. After lots of research I found several post stating that you cannot filter within include stattements and that it is an all or nothing kind of deal. Filtering at the end gives strange results all the time.

Sources: Entity Framework - Selective Condition on Included Navigation Property

Does Include load all related entities or the specified ones?

I was able to find 2 solutions: DynamicSelectExtensions: https://github.com/thiscode/DynamicSelectExtensions

There seems to beEntityFramework+ (EF+). I have seen post of the developers on Stackoverflow, although I cannot find them at the moment. His library seems to be able to do what I want to do, but normal EF is not and it seems it will never be, as the feature request for this dates back several years.

Thank you everyone for trying to help me and investing your time. For now, I will go with multiple queries and maybe check back at a later time, hopefully with more exqerience :D

soomon
  • 396
  • 2
  • 18