EF 4.3.1 Code First Updating Related Tables - asp.net-mvc-3

Update:
This is now driving me crazy!
After much Googling etc. I am really no closer to a solution.....
However I have found one thing that is puzzling me even more - the "States" of the entities just before the m_dbContext.SaveChanges() call. (see below for full repository code)
var updateInfoState = m_dc.Entry(oldPage.UpdateInfo).State; // State is 'Modified'
var oldPageState = m_dc.Entry(oldPage).State; // State is 'Detached'
this.m_dc.SaveChanges();
Why is "oldPage" detached?
Getting quite desperate now!! ;)
Original:
I appear to be having a problem with EF Code-First updating related tables correctly.
In this simplified example, the 'UpdateInfo' table IS being updated OK with the new DateTime .... but the 'Pages' table is not being updated with the new 'Name' value.
I am seeding code-first POCOs via DropCreateDatabaseAlways / override Seed ... and EF is creating the test tables correctly - so at this point it seems to know what it is doing....
I am sure this is something simple/obvious I am missing!
All help very much appreciated!
My Class definitions:
public class Page
{
public int Id { get; set; }
public string Name { get; set; }
public virtual UpdateInfo UpdateInfo { get; set; } // virtual For Lazy loading
}
[Table("UpdateInfo")] // much better than EF's choice of UpdateInfoes!
public class UpdateInfo
{
public int Id { get; set; }
public DateTime DateUpdated { get; set; }
}
public class DomainContext : DbContext
{
public DbSet<Page> Pages { get; set; }
public DbSet<UpdateInfo> UpdateInfo { get; set; }
}
Tables created by Code-First
Pages Table
===========
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](max) NULL,
[UpdateInfo_Id] [int] NULL,
UpdateInfo Table
================
[Id] [int] IDENTITY(1,1) NOT NULL,
[DateUpdated] [datetime] NOT NULL,
My Repository code:
public Page Get(int id)
{
Page page = m_dbContext.Pages.Single(p => p.Id == id);
return page;
}
public void Update(PagePostModel model)
{
Page oldPage = Get(model.PageModel.Id); // on oldPage Name = "Hello", DateUpdated = "Last Year"
Page newPage = Mapper.Map<PageModel, Page>(model.PageModel); // on newPage Name = "Goodbye" (AutoMapper)
newPage.UpdateInfo = oldPage.UpdateInfo; // take a copy of the old UpdateInfo since its not contained in the model
newPage.UpdateInfo.DateUpdated = DateTime.UtcNow; // update to now
oldPage = newPage; // copy the updated page we grabbed from dbContext above (NB Everything looks great here..oldPage is as expected)
m_dbContext.SaveChanges(); // update - only the 'UpdateInfo' table is being updated - No change to 'Pages' table :(((
}

As you know, there is a change tracker api in Entity Framework.
To track the changes of your entities you retrieved from the database, DbContext uses its reference value.
Your "Update" function above inserts newPage into oldPage. So, DbContext never knows oldPage is a newPage. So, it is "detached".
However, for UpdateInfo, it is copy of reference in oldPage, so DbContext can track change of that. So, it is "modified".
To solve this problem, how about using the code below?
Page newPage = Mapper.Map<PageModel, Page>(model.PageModel);
oldPage.UpdateInfo = newPage.UpdateInfo;
oldPage.UpdateInfo.DateUpdated = DateTime.UtcNow;
m_dbContext.SaveChanges();
Update
Then, use Attach & Detach methods.
Those methods help you attach and detach entities from DbContext.
Page newPage = Mapper.Map<PageModel, Page>(model.PageModel);
// if you attach first, there will be an exception,
// because there is two entities having same id.
m_dbContext.Entry(oldPage).State = EntityState.Detached;
m_dbContext.Pages.Attach(newPage);
// if you don't set IsModified = true,
// DbContext cannot know it is changed.
m_dbContext.Entry(newPage).State = EntityState.Modified;
m_dbContext.SaveChanges();

Related

net core API controller is returning incomplete json

I asked a question a few days ago regarding a net core game I'm making that is using Entity Framework.
I had one issue where a controller was returning duplicate JSON data.
Based on one of the answers, I changed that controller to this:
[HttpGet("GetDungeonAndRoomData/{dungeonId}")]
public async Task<ActionResult<GameDungeon>> GetDungeonAndRoomData(Guid dungeonID)
{
{
var dungeon = await _context.DungeonList
.Select(c => new GameDungeon
{
DungeonId = c.DungeonId,
DungeonName = c.DungeonName,
StartRoom = c.StartRoom,
Rooms = c.Rooms.Select(n => new GameDungeonRoom
{
RoomId = n.RoomId,
RoomText = n.RoomText,
TreasureId = n.TreasureId
})
}).SingleOrDefaultAsync(c => c.DungeonId == dungeonID);
Since I changed the controller, I had to modify this model class, so I added a new property called Rooms.
public partial class GameDungeon
{
[Key]
public string DungeonId { get; set; }
public string DungeonName { get; set; }
public string StartRoom { get; set; }
public IEnumerable<GameDungeonRoom> Rooms { get; set; }
}
Since I added that new "Rooms" property, I had to create a new model called GameDungeonRoom:
public partial class GameDungeonRoom
{
public Guid DungeonId { get; set; }
[Key]
public string RoomId { get; set; }
public string RoomText { get; set; }
public string TreasureId { get; set; }
}
Building and running the game, I now get one set of dungeon data, but it is not returning any rooms.
At first, and based off this Stack Overflow question, .net core : incomplete JSON response,I thought it was because I needed to add this to my Startup.cs file:
services.AddMvc()
.AddJsonOptions(
options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
But that was not the case.
So I then spent the evening trying a bunch of different ways of writing the controller, but they either produced the same results or just threw an error.
Reviewing the code this morning, I realized something. In my controller, the first select statement that creates the new "GameDungeon" is getting data from _context.DungeonList.
DungeonList is a model generated by Entity Framework from a real table in my database.
But GameDungeonRoom is just a new class model I created. It's based off a table in my database called RoomList, but there is nothing in _context that specifically handles GameDungeonRoom.
So I am wondering, should I introduce another controller that kind of looks like this?
var rooms = await _context.RoomList
.Select(c => new GameDungeonRoom ...
And then somehow append that to the GameDungeon object.
I sort of tried that after lunch but ended up just creating a mess of code that created an even bigger mess of errors so I just deleted it all.
Anyway, after all that, this is where my JSON currently stands:
{
"dungeonId" : "293hf938",
"dungeonName" : "Dungeon of Dread",
"startRoom" : "bjgh39811ffr",
"roomId" : "fgf4h635j",
"roomText" : "A big empty room",
"treasureId" : "12a",
"rooms": [
You'll notice that "rooms" is empty. I'm not quite sure why it is, or what's going on.
One idea I had, is maybe I should just create an API controller that get's the dungeon data for a particular dungeon. Then create another API controller that gets the Room data for a particular dungeon.
Then let the client call both controllers(using the same dungeonId) and combine the data on the client side.
So I was wondering if anyone could think of an idea as to why the "rooms" object is empty.
Thanks!
Just guessing you might have hit a cyclic reference in your result set due to Data Context being cached. Hence Json serializer cannot serialize it properly and give incomplete json content. So can you try following to pin point that.
var dungeon = await _context.DungeonList
.Select(c => new GameDungeon
{
DungeonId = c.DungeonId,
DungeonName = c.DungeonName,
StartRoom = c.StartRoom,
Rooms = c.Rooms.Select(n => new GameDungeonRoom
{
RoomId = n.RoomId,
RoomText = n.RoomText,
TreasureId = n.TreasureId
})
})
.AsNoTracking() //This ignore the cached data
.SingleOrDefaultAsync(c => c.DungeonId == dungeonID);

Entity Framework Many to Many works but Include does not

I have a typical many-to-many relationship with these 3 tables
[Post] (
[PostId] int, (PK)
[Content] nvarchar(max)
...
)
[Tag] (
[TagId] int, (PK)
[Name] nvarchar
...
)
[TagPost] (
[TagId] int, (PK, FK)
[PostId] int (PK, FK)
)
And, TagId and PostId are the PK and FK set on the tables accordingly etc. Then I have these classes and mapping in c#
public class Post {
public Post()
{
this.Tags = new HashSet<Tag>();
}
[Key]
public int PostId { get; set; }
...
public virtual ICollection<Tag> Tags { get; private set; }
}
public class Tag {
public Tag()
{
this.Posts = new HashSet<Post>();
}
[Key]
public int TagId { get; set; }
...
public virtual ICollection<Post> Posts { get; private set; }
}
internal class MyDbContext : DbContext
{
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Post>().ToTable("Post");
modelBuilder.Entity<Tag>().ToTable("Tag");
modelBuilder.Entity<Post>()
.HasMany(x => x.Tags)
.WithMany(x => x.Posts)
.Map(x =>
{
x.ToTable("TagPost");
x.MapLeftKey("PostId");
x.MapRightKey("TagId");
});
}
Then I have this code to query them
var list = (from p in ctx.Posts.Include(p => p.Tags)
from t in p.Tags
where ... // some of my filter conditions
select p).ToList();
This join does return the posts I was looking for, however the returned posts don't their associated tags filled in even though I have the Include there. Could someone help point out what I'm missing so that I could have the tags also return with the posts?
Thanks a lot.
The double from is a manual Join which causes the Include to be ignored as mentioned here and here. Include is also ignored for other LINQ methods like grouping and projections.
Relationship fixup generally does not work for many-to-many relationships, only for relationships which have at least one single reference at one of the ends - one-to-many or one-to-one. If you project the Posts and related Tags into another type (anonymous or named) the data will be loaded correctly but because the relationship is many-to-many EF won't create the relationship in memory automatically so that the post.Tags collection will stay empty.
To get the Include working you must remove the second from from your query and apply the where clause directly to the Post entity parameter, for example like so:
var list = (from p in ctx.Posts.Include(p => p.Tags)
where p.Tags.Any(t => t.TagId == 1)
select p).ToList();
The filter by a Tag property is specified in the expression passed into .Any which is an expression with a Tag (t) as parameter.
try selecting everything into an anonymous object (something like this)
var list = (
from p in ctx.Posts
from t in p.Tags
where ... // some of my filter conditions
select new {
Posts = p,
Tags = p.Tags
})
.ToList();
Based on the feedback to my initial answer and the fact that EF can find the related entities but it is failing to populate the Tags collection I believe the issue lies in the definition of the Tags entity in the Post class.
Try removing the Hashset<> initialiser from the constructors and private from the set declaration:
public virtual ICollection<Tag> Tags { get; set; }

implementing dropdownlist in asp.net mvc 3

I am teaching myself asp .net mvc3. I have researched a lot but the more I read the more confused I become. I want to create a page where users can register their property for sale or rent.
I have created a database which looks like this:
public class Property
{
public int PropertyId { get; set; }
public int PropertyType { get; set; }
ยทยทยท
public int Furnished { get; set; }
...
}
Now, I want dropdownlistfor = PropertyType and Furnished.
Property type would be
1 Flat
2 House
3 Detached House
...
Furnished would be:
1 Furnished
2 UnFurnished
3 PartFurnished
...
Now, I am really not sure where to keep this information in my code. Should I have 2 tables in my database which store this lookup? Or should I have 1 table which has all lookups? Or should I just keep this information in the model?
How will the model bind to PropertyType and Furnished in the Property entity?
Thanks!
By storing property types and furnished types in the database, you could enforce data integrity with a foreign key, rather than just storing an integer id, so I would definitely recommend this.
It also means it is future proofed for if you want to add new types. I know the values don't change often/will never change but if you wanted to add bungalow/maisonette in the future you don't have to rebuild and deploy your project, you can simply add a new row in the database.
In terms of how this would work, I'd recommend using a ViewModel that gets passed to the view, rather than passing the database model directly. That way you separate your database model from the view, and the view only sees what it needs to. It also means your drop down lists etc are strongly typed and are directly in your view model rather than just thrown into the ViewBag. Your view model could look like:
public class PropertyViewModel
{
public int PropertyId { get; set; }
public int PropertyType { get; set; }
public IEnumerable<SelectListItem> PropertyTypes { get; set; }
public int Furnished { get; set; }
public IEnumerable<SelectListItem> FurnishedTypes { get; set; }
}
So then your controller action would look like:
public class PropertiesController : Controller
{
[HttpGet]
public ViewResult Edit(int id)
{
Property property = db.Properties.Single(p => p.Id == id);
PropertyViewModel viewModel = new PropertyViewModel
{
PropertyId = property.Id,
PropertyType = property.PropertyType,
PropertyTypes = from p in db.PropertyTypes
orderby p.TypeName
select new SelectListItem
{
Text = p.TypeName,
Value = g.PropertyTypeId.ToString()
}
Furnished = property.Furnished,
FurnishedTypes = from p in db.FurnishedTypes
orderby p.TypeName
select new SelectListItem
{
Text = p.TypeName,
Value = g.FurnishedTypeId.ToString()
}
};
return View();
}
[HttpGet]
public ViewResult Edit(int id, PropertyViewModel propertyViewModel)
{
if(ModelState.IsValid)
{
// TODO: Store stuff in the database here
}
// TODO: Repopulate the view model drop lists here e.g.:
propertyViewModel.FurnishedTypes = from p in db.FurnishedTypes
orderby p.TypeName
select new SelectListItem
{
Text = p.TypeName,
Value = g.FurnishedTypeId.ToString()
};
return View(propertyViewModel);
}
}
And your view would have things like:
#Html.LabelFor(m => m.PropertyType)
#Html.DropDownListFor(m => m.PropertyType, Model.PropertyTypes)
I usually handle this sort of situation by using an enumeration in code:
public enum PropertyType {
Flat = 1,
House = 2,
Detached House = 3
}
Then in your view:
<select>
#foreach(var val in Enum.GetNames(typeof(PropertyType)){
<option>val</option>
}
</select>
You can set the id of the option equal to the value of each item in the enum, and pass it to the controller.
EDIT: To directly answer your questions:
You can store them as lookups in the db, but for small unlikely to change things, I usually just use an enum, and save a round trip.
Also look at this approach, as it looks better than mine:
Converting HTML.EditorFor into a drop down (html.dropdownfor?)

Returning a specified type from a method with EF

How can I return return a collection in a method from a LINQ query that has a one to many relationship?
For instance, I have the following code where I can have many Projects to a TimeTracking object. Will the type that I have defined for the return type (IEnumerable) work? It is set up in my EF model as this specific kind of relationship.
public IEnumerable<TimeTracking> GetTimeTrackings()
{
YeagerTechEntities DbContext = new YeagerTechEntities();
DbContext.Configuration.ProxyCreationEnabled = false;
DbContext.Configuration.LazyLoadingEnabled = false;
var timeTrackings = (from timeTrackingProjects in DbContext.TimeTrackings.Include("TimeTrackings.Projects")
select timeTrackingProjects).Where(p => p.TimeTrackingID > 0);
CloseConnection(DbContext);
return timeTrackings;
}
If so, when I display it in my MVC 3 View, and my View contains an IEnumerable<YeagerTech.YeagerTechWcfService.TimeTracking> model, will the model variable have records in it for the TimeTracking and Project objects? I don't think it will. My TimeTracking object is set up as follows unless I need to inherit the Project class with it (which would then have the Project properties):
public partial class TimeTracking
{
[DataMember]
public int TimeTrackingID { get; set; }
[DataMember]
public short ProjectID { get; set; }
[DataMember]
public byte[] Attachment { get; set; }
[Required]
[DataType(DataType.Date)]
[DataMember]
public System.DateTime StartDate { get; set; }
[Required]
[DataType(DataType.Date)]
[DataMember]
public System.DateTime EndDate { get; set; }
[DataMember]
public string Notes { get; set; }
[DataMember]
public System.DateTime CreatedDate { get; set; }
[DataMember]
public Nullable<System.DateTime> UpdatedDate { get; set; }
[DataMember]
public virtual Project Project { get; set; }
}
I also want my View to display the Project text that is associated with the TimeTracking and not the Project value. How can I do this?Can someone please help?
I got the following msg from invoking a method on my WCF client.
'cannot be serialized if reference tracking is disabled'
After getting the msg, I then modified my DataContracts to include references ([DataContract(IsReference = true)]).
namespace YeagerTechModel
{
[Serializable]
[DataContract(IsReference = true)]
public partial class Customer
{
public Customer()
{
this.Projects = new HashSet<Project>();
}
[DataMember]
public short CustomerID { get; set; }
[Required]
[StringLength(50)]
[DataType(DataType.EmailAddress)]
[DataMember]
public string Email { get; set; }
I am executing the following server side code to successfully get data from my database in a parent/child relationship. The Include method explicity invokes getting the related Project data for the specific Customer. I had to do it this way because you must turn LazyLoading off if you want to get your parent/child data across the wire.
If I look at the WCF messagelog, I can see the actual data coming across in a Customer object and it has the Project object inside of the Customer object.
However, after the call is made and I actually inspect the contents of the "customer" variable, I don't see any refernces to any Project data.
public IEnumerable<Customer> GetCustomers()
{
YeagerTechEntities DbContext = new YeagerTechEntities();
DbContext.Configuration.ProxyCreationEnabled = false;
DbContext.Configuration.LazyLoadingEnabled = false;
IQueryable<Customer> customer = DbContext.Customers.Include("Projects").Where(p => p.CustomerID > 0);
CloseConnection(DbContext);
return customer;
}
The thing I want to do now, is reference the Project data coming back from the call. However, I don't get any Customer object intellisense after typing "customer.". It's all pertains to an IQueryable object.
I'm passing it back into my MVC Controller as the following type:
IEnumerable<YeagerTechWcfService.Customer> customerList = db.GetCustomers();
and into my View as the following model:
IEnumerable<YeagerTech.YeagerTechWcfService.Customer>
Now, the big question is "How can I reference the Project data coming back in my View?
The below is my code for the View, but there is no intellisense for "item.Project". Note that "Email" is a property inside my Customer object.
foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Email)
</td>
Looks like your Linq query should be closer to this (NOTE: did not test the query, might need tweaking):
var query = (from tt in DbContext.TimeTrackings.Include("Projects")
where tt.TimeTrackingID > 0
select tt).ToList();
Linq query as you have written is deferred execution, you are closing your connection before retrieving the data, so that would probably cause a runtime error.
.Include() statement should specify the property on the entity (TimeTracking in your case) that need to be loaded, so in this case that would be Project property
Once you have retrieved your enumerable collection of TimeTracking entities you can access the properties of the Project entity associated with a particular TimeTracking entity like so:
foreach(var tracking in GetTimeTrackings())
{
foreach(var project in tracking.Projects)
{
// Assuming your Project entity has a Name property
Response.Write(project.Name);
}
}
I'm not sure what you mean by
I also want my View to display the Project text that is associated
with the TimeTracking and not the Project value.
can you clarify what property from which entity you want to see? What is the Project Entity definition?
In response to your comment about closing connection after retrieving the data:
The statement IQueryable<Customer> customer = DbContext.Customers.Include("Projects").Where(p => p.CustomerID > 0); does not actually execute a query against the database until you start to iterate it (most likely in your view with a foreach statement). If you add a .ToList() at the end of that statement, it will execute it and return a List<Customer> (which is also IEnumerable) which contains all the records that are result of your query.
When you try to type customer. to get intellisense for the Customer entity, you're not seeing it because customer is a list of Customer entities (or rather an IQueryiable of them) so you would need to do something like customer[0]. to access the properties of the first Customer entity in that list (or iterate over it).
I'm not 100% sure how entity references come through in ASP.NET MVC on a model entity but a really simple way you can get this done is create a model class you want to use in your view, say something like this:
public class TimeTrackingModel {
public int TimeTrackingID { get; set; }
public string ProjectName { get; set; }
}
then in your query do this:
var customers = (from tt in DbContext.TimeTrackings.Include("Projects")
where tt.TimeTrackingID > 0
select new TimeTrackingModel { TimeTrackingID = tt.TimeTrackingID, ProjectName = tt.Project.ProjectName }).ToList();
then in your view specify IEnumberable<TimeTrackingModel> as the model and then access the properties like this:
foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.ProjectName)
</td>
Actually, after further review, I can now see the Project collection in my Customer collection all the way back up to my client after adding a QucikWatch on the object.
The correct answer is the last part of my post where the LazyLoadingEnabled = false appears.

LinqToSQl and the Member access not legal on type exception

The basic problem...
I have a method which executes the following code:
IList<Gig> gigs = GetGigs().WithArtist(artistId).ToList();
The GetGigs() method gets Gigs from my database via LinqToSql...
So, when GetGigs().WithArtist(artistId).ToList() is executed I get the following exception:
Member access 'ListenTo.Shared.DO.Artist Artist' of 'ListenTo.Shared.DO.Act' not legal on type 'System.Collections.Generic.List`1[ListenTo.Shared.DO.Act]
Note that the extension function "WithArtist" looks like this:
public static IQueryable<Gig> WithArtist(this IQueryable<Gig> qry, Guid artistId)
{
return from gig in qry
where gig.Acts.Any(act => (null != act.Artist) && (act.Artist.ID == artistId))
orderby gig.StartDate
select gig;
}
If I replace the GetGigs() method with a method that constructs a collection of gigs in code (rather than from the DB via LinqToSQL) I do NOT get the exception.
So I'm fairly sure the problem is with my LinqToSQl code rather than the object structure.
However, I have NO IDEA why the LinqToSQl version isnt working, so I've included all the associated code below. Any help would be VERY gratefully receivced!!
The LinqToSQL code....
public IQueryable<ListenTo.Shared.DO.Gig> GetGigs()
{
return from g in DBContext.Gigs
let acts = GetActs(g.ID)
join venue in DBContext.Venues on g.VenueID equals venue.ID
select new ListenTo.Shared.DO.Gig
{
ID = g.ID,
Name = g.Name,
Acts = new List<ListenTo.Shared.DO.Act>(acts),
Description = g.Description,
StartDate = g.Date,
EndDate = g.EndDate,
IsDeleted = g.IsDeleted,
Created = g.Created,
TicketPrice = g.TicketPrice,
Venue = new ListenTo.Shared.DO.Venue {
ID = venue.ID,
Name = venue.Name,
Address = venue.Address,
Telephone = venue.Telephone,
URL = venue.Website
}
};
}
IQueryable<ListenTo.Shared.DO.Act> GetActs()
{
return from a in DBContext.Acts
join artist in DBContext.Artists on a.ArtistID equals artist.ID into art
from artist in art.DefaultIfEmpty()
select new ListenTo.Shared.DO.Act
{
ID = a.ID,
Name = a.Name,
Artist = artist == null ? null : new Shared.DO.Artist
{
ID = artist.ID,
Name = artist.Name
},
GigId = a.GigID
};
}
IQueryable<ListenTo.Shared.DO.Act> GetActs(Guid gigId)
{
return GetActs().WithGigID(gigId);
}
I have included the code for the Act, Artist and Gig objects below:
public class Gig : BaseDO
{
#region Accessors
public Venue Venue
{
get;
set;
}
public System.Nullable<DateTime> EndDate
{
get;
set;
}
public DateTime StartDate
{
get;
set;
}
public string Name
{
get;
set;
}
public string Description
{
get;
set;
}
public string TicketPrice
{
get;
set;
}
/// <summary>
/// The Act object does not exist outside the context of the Gig, therefore,
/// the full act object is loaded here.
/// </summary>
public IList<Act> Acts
{
get;
set;
}
#endregion
}
public class Act : BaseDO
{
public Guid GigId { get; set; }
public string Name { get; set; }
public Artist Artist { get; set; }
}
public class Artist : BaseDO
{
public string Name { get; set; }
public string Profile { get; set; }
public DateTime Formed { get; set; }
public Style Style { get; set; }
public Town Town { get; set; }
public string OfficalWebsiteURL { get; set; }
public string ProfileAddress { get; set; }
public string Email { get; set; }
public ImageMetaData ProfileImage { get; set; }
}
public class BaseDO: IDO
{
#region Properties
private Guid _id;
#endregion
#region IDO Members
public Guid ID
{
get
{
return this._id;
}
set
{
this._id = value;
}
}
}
}
I think the problem is the 'let' statement in GetGigs. Using 'let' means that you define a part of the final query separately from the main set to fetch. the problem is that 'let', if it's not a scalar, results in a nested query. Nested queries are not really Linq to sql's strongest point as they're executed deferred as well. In your query, you place the results of the nested query into the projection of the main set to return which is then further appended with linq operators.
When THAT happens, the nested query is buried deeper into the query which will be executed, and this leads to a situation where the nested query isn't in the outer projection of the query to execute and thus has to be merged into the SQL query ran onto the DB. This is not doable, as it's a nested query in a projection nested inside the main sql query and SQL doesn't have a concept like 'nested query in a projection', as you can't fetch a set of elements inside a projection in SQL, only scalars.
I had the same issue and what seemed to do the trick for me was separating out an inline static method call that returned IQueryable<> so that I stored this deferred query into a variable and referenced that.
I think this is a bug in Linq to SQL but at least there is a reasonable workaround. I haven't tested this out yet but my assumption is that this problem may arise only when referencing static methods of a different class within a query expression regardless of whether the return type of that function is IQueryable<>. So maybe it's the class that holds the method that is at the root of the problem. Like I said, I haven't been able to confirm this but it may be worth investigating.
UPDATE: Just in case the solution isn't clear I wanted to point it out in context of the example from the original post.
public IQueryable<ListenTo.Shared.DO.Gig> GetGigs()
{
var acts = GetActs(g.ID); // Don't worry this call is deferred
return from g in DBContext.Gigs
join venue in DBContext.Venues on g.VenueID equals venue.ID
select new ListenTo.Shared.DO.Gig
{
ID = g.ID,
Name = g.Name,
Acts = new List<ListenTo.Shared.DO.Act>(acts),
Description = g.Description,
StartDate = g.Date,
EndDate = g.EndDate,
IsDeleted = g.IsDeleted,
Created = g.Created,
TicketPrice = g.TicketPrice,
Venue = new ListenTo.Shared.DO.Venue {
ID = venue.ID,
Name = venue.Name,
Address = venue.Address,
Telephone = venue.Telephone,
URL = venue.Website
}
};
}
Note that while this should correct the issue at hand there also seems to be another issue in that the deferred acts query is being accessed in each element of the projection which I would guess would cause separate queries to be issued to the database per row in the outer projection.
I don't see anything in your classes to indicate how LINQ to SQL is meant to work out which column is which, etc.
Were you expecting the WithArtist method to be executed in .NET, or converted into SQL? If you expect it to be converted into SQL, you'll need to decorate your Gig class with appropriate LINQ to SQL attributes (or configure your data context some other way). If you want it to be executed in code, just change the first parameter type from IQueryable<Gig> to IEnumerable<Gig>.
I found out that an issue like this (which I also had recently) can be resolved, if you convert the IQueryable (or Table) variable Gigs into a list like so
return from g in DBContext.Gigs.ToList()
...
If that still doesn't work, do the same for all the IQueryables. The reason behind seems to me that some queries are too complex to be translated into SQL. But if you "materialize" it into a list, you can do every kind of query.
Be careful, you should add "filters" (where conditions) early because too much memory consumption can become a problem.

Resources