Is LINQ implementation of Entity Framework inappropriate? - c#

Tested objects' classes as preamble
namespace EFConsoleApp.Models.Db
{
[Table("a")]
public class TableA
{
[Key]
[Column("id")]
public int Id { get; set; }
[Column("title")]
public string Title { get; set; }
[Column("amount")]
public int Amount { get; set; }
[Column("a_id")]
public int AId { get; set; }
}
}
namespace EFConsoleApp.DataAccesses.Db
{
public class ContextA : DbContext
{
public string DefaultSchema { get; private set; }
public ContextA() : base(GetConnecting(), true)
{
DefaultSchema = "public";
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
=> modelBuilder.HasDefaultSchema(DefaultSchema);
public static NpgsqlConnection GetConnecting()
{
return new NpgsqlConnection(ConfigurationManager.ConnectionStrings["postgreSql"].ToString());
}
public virtual DbSet<TableA> A { get; set; }
}
}
Question
Why I get the following exception when executing the code below with no data in table?
※ I'm using Entity Framework 6.2.0 and Npgsql 4.0.7 and .NET Framework 4.6 .
Exception:
System.InvalidOperationException:
The cast to value type 'System.Int32' failed because the materialized value is null.
Either the result type's generic parameter or the query must use a nullable type.
Executed code:
// _ctx's type is DataAccesses.Db.ContextA .
// ContextA table has no data.
var sum = _ctx.A
.Where(e => e.AId == aid)
.Select(e => e.Amount)
// .ToString() shows "SELECT \"Extent1\".\"amount\" FROM \"public\".\"a\" AS \"Extent1\" WHERE \"Extent1\".\"a_id\" = #p__linq__0"
.Sum();
And why I don't get the same exception when executing the code below with no data in list?
var list = new List<TableA>();
var qList = list.AsQueryable(); // To imitate return type of .Select()
// qList has no data.
var sum = qList
.Where(e => e.AId == 1)
.Select(e => e.Amount)
.Sum(); // sum = 0
Called LINQ methods are the same (as I think...), but result is different.
There are also other attempts
Attempt #1
var sum = <Resource>
.Where(e => e.AId == a
.Sum(e => e.Amount);
In case of EF, the same exception.
In case of in-memory object (new List<TableA>().AsQueryable()), sum is 0.
Attempt #2
var sum = (from a in <Resource> where a.AId == aid select a.Amount).Sum();
In case of EF, the same exception.
In case of in-memory object (new List<TableA>().AsQueryable()), sum is 0.
Attempt #3 (Tested only with EF)
var sum = _ctx.Database
.SqlQuery<int>("SELECT amount FROM a WHERE a_id = #par_a_id;",
new NpgsqlParameter("par_a_id", aid))
.Sum();
Surprisingly it works and sum is 0.
So, what's wrong with EF's implementation of LINQ?
Or problem with me because I can't understand something how it works under hood?
Update against #MindSwipe 's answer
I tried to execute sql with SUM directly in database and EF.
※ Still with no data in table.
SELECT SUM(amount) FROM a WHERE a_id = 1;
Query above returns null.
var sum = _ctx.Database
.SqlQuery<int>("SELECT SUM(amount) FROM a WHERE a_id = #par_a_id;",
new NpgsqlParameter("par_a_id", aid))
.Single();
Code above cause the same exception.
System.InvalidOperationException:
The cast to value type 'System.Int32' failed because the materialized value is null.
Either the result type's generic parameter or the query must use a nullable type.
So, at least I understood why exception occurs.
As conclusion
In case to avoid exceptions when working with SUM,
obviously, the code below is the most safe?
var sum = _ctx.Database
.Where(e => e.AId == aid)
.Select(e => e.Amount)
.DefaultIfEmpty(0)
.Sum();
Update against #Aron 's comment
Using IDbCommandInterceptor I got raw sql.
var sum = _ctx.A
.Where(e => e.AId == aid)
.Select(e => e.Amount) // #1
.Sum(); // #2
-- #1
SELECT "Extent1"."amount" FROM "public"."a" AS "Extent1" WHERE "Extent1"."a_id" = #p__linq__0"
-- #2
SELECT "GroupBy1"."A1" AS "C1" FROM (SELECT CAST (sum("Extent1"."amount") AS int4) AS "A1" FROM "public"."a" AS "Extent1" WHERE "Extent1"."a_id" = #p__linq__0) AS "GroupBy1"

Edit: Major re-write of the answer, the old one was incorrect.
In PostgresSQL SUM returns null when there are no elements to sum up. To test this, here's a short script:
drop table if exists temp;
create table temp (id integer, amount integer);
insert into temp (id, amount) values (1, 0);
select SUM(amount) from temp where id = 2;
Check the output and it will be null, not 0 or any other number
You can copy and paste the script into this site if you don't want to use a local database.
To protect against this case you need to either
Cast select nullable amount, and sum it, returning 0 if none were found, like so:
var sum = _ctx.A
.Where(x => x.AId == aid)
.Select(x => (int?) x.Amount)
.Sum() ?? 0;
Make Amount nullable by making it be public int? Amount {get; set;}

Related

Returning IEnumerable<> for a model in ASP.NET Web API

I am trying to get a list whose type is a model(called ItemDetail). However, I get this error: "Object reference not set to an instance of an object."
public class ItemDetail
{
public decimal sum { get; set; }
public string username { get; set; }
public string units { get; set; }
public string remarks { get; set; }
}
The API code is as follows:
[HttpGet]
[Route("api/items/details/{id}")]
public IEnumerable<ItemDetail> ItemDetails(int id)
{
using (ShopEntities entities = new ShopEntities())
{
var itemDetails = entities.vwItemDetails.ToList();
var userIds = from data in itemDetails
select data.Userid;
var item_Details = new List<ItemDetail> ();
foreach (int userId in userIds)
{
int totals = (int)entities.vwItemDetails
.Where(i => i.UserID == userId & i.item_id == id)
.Select(i => i.quantity)
.DefaultIfEmpty(0)
.Sum();
var itemRecord = entities.vwItemDetails.SingleOrDefault(i => i.item_id == id & i.Userid == userId);
item_Details.Append(new ItemDetail
{
username = itemRecord.username,
units = itemRecord.units,
remarks = itemRecord.Remarks,
sum = totals
});
}
return item_Details;
}
}
Thanks in advance for your help.
EDIT: The error occurs inside the foreach loop where I'm trying to append the new ItemDetail (item_Details.Append(new ItemDetail)
I think I see the problem...
var itemRecord = entities.vwItemDetails.SingleOrDefault(i => i.item_id == id & i.Userid == userId);
Your filtering for the SingleOrDefault() is using a bitwise AND operator instead of boolean one. The value of itemRecord as it's written right now is almost certainly always null. Try changing that line to this:
var itemRecord = entities.vwItemDetails.SingleOrDefault(i => i.item_id == id && i.Userid == userId);
EDIT:
I just realized you do the same thing in this LINQ area:
int totals = (int)entities.vwItemDetails
.Where(i => i.UserID == userId & i.item_id == id)
.Select(i => i.quantity)
.DefaultIfEmpty(0)
.Sum();
Again, totals is probably coming up as 0.
EDIT 2:
There is more wrong here than I original anticipated. I created a semi-complete working sample and you've got problems beyond the use of the bitwise operator.
As orhtej2 pointed out in the comments, you are setting yourself up to return null, but you don't check for it. So that is the immediate cause of your exception. You're probably iterating through a list of user IDs where some of the IDs aren't linked to the item ID you're working with. That will return a null because of SingleOrDefault.
The fix for that is to check if itemRecord is null, and if so, do continue; to move onto the next user ID. Or if you want to stop processing and return an error, do that. Either way the situation should be handled.
Related to that is another consequence of using SingleOrDefault. A friend of mine pointed out that if you end up with more than one result in your where clause there, you will get an exception as well. Unless you can guarantee that no single user ID will have more than one instance of a given item ID in that collection of item details, you need to use a different method. The most straightforward would be to use Where() instead, and handle the IEnumerable<> that it returns. You'll have another loop, but that's showbiz.

Linq Method Error IOrderedQueryable

I have a database with a specific id with recorded Time's, I need help on trying to figure out time gap's between an ID's time's e.g 13:05:15 and 13:05:45 though if the time gap is over 10/15 seconds it needs to be recorded so it can be used in say a text file/other data etc. I previously asked a similar question on here, here is what my code looks like so far:
This class is used to manipulate data through the linq var being queried/looped
public class Result
{
public bool LongerThan10Seconds { get; set; }
public int Id { get; set; }
public DateTime CompletionTime { get; set; }
}
This is the foor loop within a separate class which was my original idea
using (var data = new ProjectEntities())
{
Result lastResult = null;
List<Result> dataResults = new List<Result>();
foreach(var subResult in data.Status.Select(x => x.ID).Distinct().Select(Id => data.Status.Where(x => x.ID == Id).OrderBy(x => x.Time)))
{
if (lastResult != null)
{
if (subResult.CompletionTime.Subtract(lastResult.CompletionTime).Seconds > 10)
dataResults.Add(subResult);
}
lastResult = subResult;
}
Which I got the error:
Linq.IOrderedQueryAble does not contain a definition for 'CompletionTime' and no Extension method 'CompletionTime' accepting a first argument of type 'System.Linq.IOrderedQueryable.
I changed the for loop to use an object of the manipulation class
foreach(Result subResult in data.AssetStatusHistories.Select(x => x.ID).Distinct().SelectMany(Id => data.AssetStatusHistories.Where(x => x.ID == Id).OrderBy(x => x.TimeStamp)))
{
if (lastResult != null)
{
if (subResult.CompletionTime.Subtract(lastResult.CompletionTime).Seconds > 10)
{
vehicleResults.Add(subResult);
}
}
lastResult = subResult;
}
Though now I get the error: Cannot convert type 'Project.Status' to 'Project.Result'
Does anyone possibly have a solution to get around this I have looked through a few resources but haven't been able to find anything of helps also even on Microsoft's Linq Forum. Any help is much appreciated ! :)
Try adding .ToList() to the end of your LINQ statement, after OrderBy:
var results = data.Status.Select(x => x.ID).Distinct()
.Select(Id => data.Status.Where(x => x.ID == Id)
.OrderBy(x => x.Time)
.ToList();
foreach(var subResult in results))
{
...
}
Also, I think you could modify your LINQ to do a GroupBy of the ID column, but that's something you could do research on if you wish. (Tutorial)
Your linq query (in the second try) will return an IEnumerable of whatever is the element type of data.AssetStatusHistories. I assume this is some kind of IEnumerable<Project.Status>, so you're in fact trying to assign Project.Status objects to an iterator variable (subResult) of type Result, which is why you're getting the error Cannot convert type 'Project.Status' to 'Project.Result'.
So your problem is not really in the linq query, you just need a way to convert your Project.Status objects to Project.Result objects.

LINQ to Entities does not recognize the method 'Int32 Parse(System.String)' method when attempting to parse a column for inequality comparisons

I have following code in my page:
var myVar= Entity.SetName
.Where(p => int.Parse(p.ID) >= start &&
int.Parse(p.ID) <= end);
start and end are int, but p.ID is string. So i should convert p.ID to int. But i get following error:
LINQ to Entities does not recognize the method 'Int32
Parse(System.String)' method, and this method cannot be translated
into a store expression.
Where is the problem??
First, I would highly recommend to check your database design, whether there is a really good reason for ID to be a string. I would consider changing the ID DB type to int and you will get rid of this problem with converting.
The error you get means, that EF does not know how to convert the method Int32.Parse() to SQL.
Basically you have two options how to deal with that:
Do the comparison outside the linq to entities:
var myVar= Entity.SetName.AsEnumerable()
.Where(p => int.Parse(p.ID) >= start &&
int.Parse(p.ID) <= end);
But this is not recommended, because you are reading whole result set from DB, before applying the where condition.
Or make custom model defined function as described in this post on SO:
Convert String to Int in EF 4.0
or
Entity Framework: Where do I extend the CSDL/MSL?
First try to convert to int then pass that variable name
int catgry = Convert.ToInt32(customercategory.OWNERSHIP_TYPE);
var customerCateg = (from d in _db.tbl_SIL_CUSTOMER_CATEGORY_MST
.Where(d => d.CAT_ID == catgry) select d.CAT_TYPE).SingleOrDefault();
private void LoadDetail(int id)
{
var sp = from category in db.ProductCategories
join product in db.Products on category.ProductCategoryID equals product.ProductCategoryID
where id == int.Parse(category.ProductCategoryID)
select new
{
product.ProductID,
product.ProductName,
product.ProductCode,
product.Deception,
category.CategoryName,
product.Quanrity,
product.Price
};
DGVDetail.DataSource = sp.ToList();//help: Error: LINQ to Entities does not recognize the method 'Int32 Parse(System.String)' method, and this method cannot be translated into a store expression
}
private void DGVMaster_CellClick(object sender, DataGridViewCellEventArgs e)
{
int index = e.RowIndex;
LoadDetail(index + 1);
}
To Convert string to int, you have to make it Enumerable, then you can make sort or anything you like
var list = db.UserEntriesTable.AsEnumerable().Select(x => new {
Name = x.Name,
Roll = Convert.ToInt32(x.Roll),
Mobile = x.Mobile
}).OrderBy(x => x.Roll).ToList();
int nb = EE.Stagaire.Join(EE.Filiere, S => S.IdFiliere, F => F.IdFiliere, (S, F) => new
{
ID = S.Id,
Name = S.Nom,
Prénon = S.Prenon,
Email = S.Email,
MoteDePass = S.MoteDePass,
Filiere = F.Filiere1,
}).Where(S => S.ID.ToString() == r.ToString()).Take(1).Count();
if (nb != 0)
{
MessageBox.Show("find it");
}
//You can do that if you have to data type is integer
Although it's not efficient, you should be able to load all rows, and then use LINQ to Objects:
var myVar= Entity.SetName.ToList()
.Where(p => int.Parse(p.ID) >= start &&
int.Parse(p.ID) <= end);

Retrieve a number from a table by EF

Entity Frame 5.0 question.
I am new to Entity Frame, basically I have a table "MyPIN".
[Table("MyPIN")]
public class BatchPINDetail
{
public BatchPINDetail();
public int Number { get; set; }
I want to run a query
int x = the biggest Number FROM MyPIN
The correspond codes are:
public class InContext : DbContext
{
public InContext();
public InContext(string connectionString);
public DbSet<BatchDetail> BatchDetailsRecords { get; set; }
}
Question 1: I am not sure how to retrieve it.
Question 2: After I get the value, I want to reassign the value, say Number += 1; I need to write it to DB.
Thanks for help.
InContext inContext = new InContext(cnStr);
// get first biggest item
BatchPINDetail entity = inContext.BatchDetailsRecords
.OrderByDescending(x => x.Number)
.First();
// get all biggest items
BatchPINDetail entities = inContext.BatchDetailsRecords
.Where(x => x.Number == x.Max(x => x.Number))
.ToArray();
// and if you just want to get biggest number.
// but note that if you just change `num` nothing will happen.
int num = inContext.BatchDetailsRecords.Max(x => x.Number);
entity.Number += 1;
inContext.SaveChanges();
You can use direct approach like the following code and avoid forth and back call to the database.
InContext inContext = new InContext(cnStr);
string Sql="SELECT * FROM MyPIN where Number in (Select MAX(Number) from MyPIN)";
BatchPINDetail[] entities = inContext.BatchDetailsRecords.SqlQuery(Sql).ToArray();
Now you can do your changes and call SaveChanges() after that.

Flattening a loop with lookups into a single linq expression

In Type member support in LINQ-to-Entities? I was attempting to declare a class property to be queried in LINQ which ran into some issues. Here I will lay out the code inside the implementation in hopes of some help for converting it to a query.
I have a class Quiz which contains a collection of Questions, each of which is classified according to a QuestionLevel... I need to determine whether a quiz is "open" or "closed", which is accomplished via an outer join on the question levels and a count of the questions in each level, as compared with a table of maximum values. Here's the code, verbatim:
public partial class Quiz
{
public bool IsClosed
{
get
{
// if quiz has no questions, it's open
if (this.Questions.Count() == 0) return false;
// get a new handle to the EF container to do a query for max values
using (EFContainer db = new EFContainer())
{
// we get a dictionary of LevelName/number
Dictionary<string, int> max = db.Registry
.Where(x => x.Domain == "Quiz")
.ToDictionary(x => x.Key, x => Convert.ToInt32(x.Value));
// count the number of questions in each level, comparing to the maxima
// if any of them are less, the quiz is "open"
foreach (QuestionLevel ql in db.QuestionLevels)
{
if (this.Questions.Where(x => x.Level == ql).Count() < max["Q:Max:" + ql.Name])
return false;
}
}
// the quiz is closed
return true;
}
}
}
so here's my not-yet-working attempt at it:
public static IQueryable<Quiz> WhereIsOpen(this IQueryable<Quiz> query)
{
EFContainer db = new EFContainer();
return from ql in db.QuestionLevels
join q in query on ql equals q.Questions.Select(x => x.Level)
into qs
from q in qs.DefaultIfEmpty()
where q.Questions.Count() < db.Registry
.Where(x => x.Domain == "Quiz")
.Where(x => x.Key == "Q:Max" + ql.Name)
.Select(x => Convert.ToInt32(x.Value))
select q;
}
it fails on account on the join, complaining:
The type of one of the expressions in the join clause is incorrect.
The type inference failed in the call to 'GroupJoin'
I'm still trying to figure that out.
* update I *
ah. silly me.
join q in query on ql equals q.Questions.Select(x => x.Level).Single()
one more roadblock:
The specified LINQ expression contains references to queries that are
associated with different contexts.
this is because of the new container I create for the maximum lookups; so I thought to re-factor like this:
public static IQueryable<Quiz> WhereIsOpen(this IQueryable<Quiz> query)
{
EFContainer db = new EFContainer();
IEnumerable<QuestionLevel> QuestionLevels = db.QuestionLevels.ToList();
Dictionary<string, int> max = db.Registry
.Where(x => x.Domain == "Quiz")
.ToDictionary(x => x.Key, x => Convert.ToInt32(x.Value));
return from ql in QuestionLevels
join q in query on ql equals q.Questions.Select(x => x.Level).Single()
into qs
from q in qs.DefaultIfEmpty()
where q.Questions.Count() < max["Q:Max:" + ql.Name]
select q;
}
but I can't get the expression to compile... it needs me to cast QuestionLevels to an IQueryable (but casting doesn't work, producing runtime exceptions).
* update II *
I found a solution to the casting problem but now I'm back to the "different contexts" exception. grr...
return from ql in QuestionLevels.AsQueryable()
* update (Kirk's suggestion) *
so I now have this, which compiles but generates a run-time exception:
public static IQueryable<Quiz> WhereIsOpen(this IQueryable<Quiz> query)
{
EFContainer db = new EFContainer();
IEnumerable<string> QuestionLevels = db.QuestionLevels.Select(x => x.Name).ToList();
Dictionary<string, int> max = db.Registry
.Where(x => x.Domain == "Quiz")
.ToDictionary(x => x.Key, x => Convert.ToInt32(x.Value));
return from ql in QuestionLevels.AsQueryable()
join q in query on ql equals q.Questions.Select(x => x.Level.Name).Single()
into qs
from q in qs.DefaultIfEmpty()
where q.Questions.Count() < max["Q:Max:" + ql]
select q;
}
which I then call like this:
List<Product> p = db.Quizes.WhereIsOpen().Select(x => x.Component.Product).ToList();
with the resulting exception:
This method supports the LINQ to Entities infrastructure and is not
intended to be used directly from your code.
The issues you're coming across are common when you couple your database objects to your domain objects. It's for this exact reason that it's good to have a separate set of classes that represent your domain and a separate set of classes that represent your database and are used for database CRUD. Overlap in properties is to be expected, but this approach offers more control of your application and decouples your database from your business logic.
The idea that a quiz is closed belongs to your domain (the business logic). Your DAL (data access layer) should be responsible for joining all the necessary tables so that when you return a Quiz, all the information needed to determine whether or not it's closed is available. Your domain/service/business layer should then create the domain object with the IsClosed property properly populated so that in your UI layer (MVC) you can easily access it.
I see that you're access the database context directly, I'd warn against that and encourage you to look into using DI/IoC framework (Ninject is great), however, I'm going to access the database context directly also
Use this class in your views/controllers:
public class QuizDomainObject
{
public int Id {get; set;}
public bool IsClosed {get; set;}
// all other properties
}
Controller:
public class QuizController : Controller
{
public ActionResult View(int id)
{
// using a DI/IoC container is the
// preferred method instead of
// manually creating a service
var quizService = new QuizService();
QuizDomainObject quiz = quizService.GetQuiz(id);
return View(quiz);
}
}
Service/business layer:
public class QuizService
{
public QuizDomainObject GetQuiz(int id)
{
// using a DI/IoC container is the
// preferred method instead of
// access the datacontext directly
using (EFContainer db = new EFContainer())
{
Dictionary<string, int> max = db.Registry
.Where(x => x.Domain == "Quiz")
.ToDictionary(x => x.Key, x => Convert.ToInt32(x.Value));
var quiz = from q in db.Quizes
where q.Id equals id
select new QuizDomainObject()
{
Id = q.Id,
// all other propeties,
// I'm still unclear about the structure of your
// database and how it interlates, you'll need
// to figure out the query correctly here
IsClosed = from q in ....
};
return quiz;
}
}
}
Re: your comment
The join to QuestionLevels is making it think there are two contexts... but really there shouldn't be because the QuestionLevels should contain in-memory objects
I believe that if you join on simple types rather than objects you'll avoid this problem. The following might work for you:
return from ql in QuestionLevels
join q in query
on ql.LevelId equals q.Questions.Select(x => x.Level).Single().LevelId
into qs
(and if this doesn't work then construct some anonymous types and join on the Id)
The problem is that joining on the Level objects causes EF to do some under-the-covers magic - find the objects in the database and perform a join there. If you tell it to join on a simple type then it should send the values to the database for a SELECT, retrieve the objects and stitch them together back in your application layer.

Categories

Resources