I'm trying to figure out how I can make a advanced search feature on my website. The code I'm using right now is not efficient and creates a really expensive query. What would be a good resource/example on creating something like this:
My Search Controller:
public ActionResult Index(string q = null, string authors = null, string category = null, decimal rating = 0, string published = null, int completed = 0, int page = 0)
{
List<string> categories = new List<string>();
List<string> authorss = new List<string>();
DateTime DateBy = new DateTime();
DateTime.TryParse(published, out DateBy);
if(!string.IsNullOrEmpty(authors))
authorss = authors.Split(',').ToList();
if (!string.IsNullOrEmpty(category))
categories = category.Split(',').ToList();
IEnumerable<Comic> Comics = db.Comics.Where(i => i.Title.Contains(q)).Include(i => i.ComicRatings).Include(i => i.ComicAuthors).Include("ComicAuthors.User");
if(authorss.Count() >= 1)
{
Comics = Comics.Where(i => i.ComicAuthors.Where(j => authorss.Contains(j.User.UserName)).GroupBy(j => j.Comic_Id).Where(j => j.Count() >= authorss.Count()).Any());
}
if (categories.Count() >= 1)
{
Comics = Comics.Where(i => i.ComicCategories.Where(j => categories.Contains(j.Category.Name)).GroupBy(j => j.Comic_Id).Where(j => j.Count() >= categories.Count()).Any());
}
if (rating != 0)
{
Comics = Comics.Where(i => i.ComicRatings.Where(j => j.Rating >= rating).Any());
}
if (completed == 1)
{
Comics = Comics.Where(i => i.Completed == false);
}
else if (completed == 2)
{
Comics = Comics.Where(i => i.Completed == true);
}
if (!string.IsNullOrEmpty(published))
{
Comics = Comics.Where(i => i.DatePublished >= DateBy);
}
if(page <= (Comics.Count() / 20))
page = 0;
Comics = Comics.Skip(page * 20).Take(20);
IEnumerable<LocalComicCategoriesModel> testing = helper.getCategories();
ViewSearchModel post = new ViewSearchModel
{
Comic = Comics.ToList(),
Categories = testing
};
return View(post);
}
If you're trying to do a lot of text searching I would take a look at Lucene.Net
Lucene is a non relational full text search engine, thats in use in a lot of places.
We spent ages trying to do text searching in sql and linq before throwing it all away and having a fully dedicated search system.
I think your main problem comes from the fact that you are retrieving too many comics and then trying to filter them. I would try to limit the numbers of comics I am retrieving from the database as a first step. To do this you can either build your query one filter at a time without actually causing it to execute (like you do with the use of Any() at the end of your calls) until the very end, or to build the query using predicate builder. Have a look at these two questions as they may provide all you need:
Creating dynamic queries with entity framework
and
Building dynamic where clauses in LINQ to EF queries
Related
I want a way to check the max and the min times a ticket has been called at a certain weekday (eg: Monday: 5 tickets max; 2 min), the ticket has an ID that ranges between 0 and 4 so I want them all to be noticed.
This is what I currently have:
int S = 0;
var senhas = _db.SenhaSet.ToList();
double[] countersemana = new double[20];
foreach (var senha in senhas)
{
if (senha.Data.DayOfWeek == DayOfWeek.Monday && senha.IdTipoSenha >= 0)
{
S++;
break;
}
}
Your code is very inefficient as it relies on returning the entire SenhaSet table from the database and then looping round it. You want to apply the filter via Entity Framework on the database. Something like (I don't know your column names so am guessing):
var senhas = _db.SenhaSet
.Where(s => SqlFunctions.DatePart("dw", s.Data) == DayOfWeek.Monday && IdTipoSenha >= 0)
.GroupBy(s => DbFunctions.TruncateTime(s.Data))
.Select(s => new {
TicketDate= s.Key,
Count = s.Count(t => t.IdSenha)
})
.ToList();
int maxTickets = senhas.Max(s => s.Count);
int minTickets = senhas.Min(s => s.Count);
This assumes the database is SQL Server in order to use the System.Data.Objects.SqlClient.SqlFunctions.DatePart method.
in my MVC Application using EntityFramework I have a self referencing table: customers with customerParentID. For children with no emails I want to be able to channel correspondence to parents Email. This is the solution I have that works but I am hitting database 3 times and I want to combine the last 2 into one.
Controller Linq Query
useParentEmail is a bool which is true for children with no Email
Id is obtained by a parameter
var b = db.Lists.SingleOrDefault(a => a.LatestTxnId == Id);
var customerWithNoEmail = db.Customers.SingleOrDefault(a => a.CustomerID == b.CustomerID && a.useParentEmail);
if (customerWithNoEmail != null)
{
var customerParent = db.Customers.SingleOrDefault(a => a.CustomerID == customerWithNoEmail.ParentCustomerID);
customerParentEmail = customerParent.Email;
}
This query hits database twice is there a better way I can do this to hit database once? Any help will be appreciated.
Assuming you are using Entity Framework with the correct navigation properties in the model so that joins work automagically, something like this could work (I've used LinqPad and linq to objects for ease of example). To be honest although I used to be a heavy user of Entity Framework I haven't used it for a few years so I could be a little rusty.
var customers = new[]
{
new { CustomerID = 1, useParentEmail = true, email = "", ParentCustomerID = (int?)2 },
new { CustomerID = 2, useParentEmail = false, email = "parent#example.org", ParentCustomerID = (int?)null },
new { CustomerID = 3, useParentEmail = false, email = "child#example.org", ParentCustomerID = (int?)null }
};
int id = 1;
var emails = (from c in customers
join p in customers on c.ParentCustomerID equals p.CustomerID into childrenParents
from cp in childrenParents.DefaultIfEmpty()
where c.CustomerID == id
select new
{
c.CustomerID,
email = c.useParentEmail == true ? cp.email : c.email
}).SingleOrDefault();
emails.Dump();
This has stumped me for the last 3 hours... I'm probably just tired but can't seem to get the logic correct. Would I'd like to do is;
get a list of survey topics and a list of rated survey topics.
if none of the topics have been rated, return the first to the user to rate.
if they've all been rated, return a view saying 'yay you completed the survey'
else identify which ones have not been rated and serve those up in a view.
All topics are served up 1 at a time, each time the topic is rated, its saved then i redirect them back to this controller.
I think my string.equals is not working but can't seem to figure out why. The controller just keeps serving up the same topic. (I'm assuming its the first record that matches vs the one that doesn't?)
public ActionResult Index(string page)
{
Rating rating = new Rating();
var surveyItems = (from s in db.Surveys
where s.Category.Name.Equals(page)
select s).ToList();
var ratedItems = (from r in db.Ratings
where r.Category.Equals(page) && r.UserName.Equals(User.Identity.Name)
select r).ToList();
if (ratedItems.Count() == 0 && surveyItems.Count() > 0)
{
ViewBag.Remaining = surveyItems.Count();
rating.Topic = surveyItems.Select(si => si.Topic).FirstOrDefault();
rating.Category = page;
return View(rating);
}
else if (ratedItems.Count() > 0 && ratedItems.Count() == surveyItems.Count())
{
return View("Finished");
}
else
{
foreach (var si in surveyItems)
{
foreach (var ri in ratedItems)
{
if (!si.Topic.Equals(ri.Topic))
{
rating.Topic = si.Topic;
rating.Category = page;
ViewBag.Total = surveyItems.Count();
ViewBag.Remaining = ViewBag.Total - ratedItems.Count();
return View(rating);
}
}
}
}
Firstly, to answer your question directly, your inner loop will always fail because the 2 lists are not ordered AND theres no gaurantee that item 1 in each list will be the same. Even if they are, the second item from the first list will not equal the first item from the second list (inner loop).
Best bet is to tackle this entirely with LINQ, and while the query is a little difficult to read, the code is a lot cleaner.
var rating = (from s in db.Surveys
join r in db.Ratingson s.Topic equals r.Topic into rated
from ri in rated.Where(x => x.Username == User.Identity.Name).DefaultIfEmpty()
where s.Category.Name.Equals(page) && ri.Topic == null
select new RatingViewModel {Topic = s.Topic, Category = s.Category, Total = db.SurveyItems.Count(), Rated = rated.Count()}).FirstOrDefault();
if (rating == null)
{
return View("Finished");
}
return View(rating);
The LINQ query is essentially the equivalent of the following SQL (give or take)
SELECT * FROM Surveys s
LEFT OUTER JOIN Ratings r ON s.Topic = r.Topic AND r.Username = 'user'
WHERE r.Topic IS NULL
You'll also note that the query projects to RatingsViewModel, I added this because I noticed you had a few references to ViewBag as well as your Rating entity.
RatingViewModel:
public class RatingViewModel
{
public string Topic { get; set; }
public string Category { get; set; }
public int Total { get; set; }
public int Rated { get; set; }
public int Remaining {
get { return Total - Rated; }
}
}
EDIT
Played around with the query a little more, and this is the closest I could get:
// define the where's here so we can use the IQueryable multiple times in LINQ
var surveys = db.Surveys.Where(x => x.Category.Name.Equals(page));
var ratedItems = db.Ratings.Where(y => y.Username == User.Identity.Name && y.Category.Name.Equals(page));
var rated = ratedItems.Count(); // get the rated count here, otherwise we end up with an exception inside the LINQ query
var surveyTopic =
(from s in surveys
// LEFT OUTER JOIN to ratings
join r in ratedItems on s.Topic equals r.Topic into ratedSurveys
from ri in ratedSurveys.DefaultIfEmpty()
// define variables
let total = surveys.Count()
//let rated = ratedItems.Count() -- this throws a NotSupportedException... which seems odd considering the line above
// get non-rated surveys, in this case the RIGHT side of the join (Ratings) is null if there is no rating
where ri.Topic == null
// projection
select new RatingViewModel
{
Topic = s.Topic,
Category = s.Category,
Rated = rated,
Total = total
}).FirstOrDefault();
return surveyTopic == null ? View("Finished") : View(surveyTopic);
Unfortunately this results in 2 DB queries which I was hoping to avoid, still this should be a little closer to what you are after.
Brent,
It didn't like your solution so I tried to revamp it a bit but it's still not happy. Here's my tweak;
var surveyTopic = (from s in db.Surveys.Where(x => x.Category.Name.Equals(page))
let total = s.Topic.Count()
join r in db.Ratings.Where(y => y.UserName == User.Identity.Name) on s.Topic equals r.Topic
let rated = r.Topic.Count()
where r.Topic == null
select new RatingViewModel
{
Topic = s.Topic,
Category = s.Category.Name,
Rated = rated+1,
Total = total
}).FirstOrDefault();
I am developing a MVC application.
I am using the two queries to fetch the record, and I want to get the common records from these queries .
I want to return the data set in list
Like this
return Json(poList, JsonRequestBehavior.AllowGet);
My two queries are..
var poList = (from po in db.PurchaseOrders
where po.CompanyId == companyId && po.PartyId == partyId && (po.IsDeleted == false || po.IsDeleted == null)
select po into newPO
select new
{
Name = newPO.PONo,
Id = newPO.Id
});
//.ToList().OrderBy(e => e.Name);
var poList2 = (db.Employees.Where(x => x.Id == EmpID)
.SelectMany(x => x.Roles)
.SelectMany(x => x.Employees)
.Distinct()
.SelectMany(x => x.PurchaseOrders)
.Select(po => new { Name = po.PONo, Id = po.Id }));
var finalPO = from PO in poList.ToList().Union(poList2).ToList() select PO);
The reason you can't union them is that the two lists return different objects.
The first list returns an anonymous type with members Name and Id. If, instead, you just wanted to return the purchase orders in query one, then you could simply use the following:
var poList = (
from po in db.PurchaseOrders
where po.CompanyId == companyId &&
po.PartyId == partyId &&
(po.IsDeleted == false || po.IsDeleted == null)
select po
);
You may need to append .ToList() to the query above in order to use the Union(...) method. Then, you should be able to union the two sequences together (assuming poList2 is also a sequence of db.PurhaseOrders objects.
Conversely, instead of changing query for poList above, you could change the query behind poList2 to the following to achieve the same effect, but different results:
var poList2 = (db.Employees.Where(x => x.Id == EmpID)
.SelectMany(x => x.Roles)
.SelectMany(x => x.Employees)
.Distinct()
.SelectMany(x => x.PurchaseOrders)
.Select(po => new { Name = po.PONo, Id = po.Id }));
Personally, I think the first one is more clear (unless there are many fields on the PO object and you only need the two as shown).
UPDATE: I see the original post was edited so that both queries now return the same object (or shape of object). However, the poster is still trying to combine the results incorrectly. The poster is using yet another LINQ query in an attempt to use the Union(...) method. This is completely unnecessary. Might as well write out the code for him/her:
var finalPO = poList.Union(poList2).ToList(); // ToList() only necessary if you need a list back
That should do it.
Really, the two books I mentioned in my comments below will get you a long way in understanding .NET and LINQ: APress - Pro C# and the .NET Framework 4.0; O'Reilly - C# 5 in a Nutshell. There are also many books on LINQ alone--but without a solid grasp of .NET (and C#, F#, or VB), you can't hope to understand or use LINQ.
I dont not think you need the ToList() in the intermediate results, just use the union and do the ToList in the final result, like:
var finalPO = poList.Union(poList2).ToList()
First, create a ViewModel like this:
public class PurchaseOrderViewModel
{
public int Id { get; set; }
public string Name { get; set; }
}
Then, use it in your code like this:
var poList1 = (from po in db.PurchaseOrders
where po.CompanyId == companyId && po.PartyId == partyId
&& (po.IsDeleted == false || po.IsDeleted == null)
select po into newPO
select new PurchaseOrderViewModel
{
Name = newPO.PONo,
Id = newPO.Id
}).ToList();
var poList2 = (db.Employees.Where(x => x.Id == EmpID)
.SelectMany(x => x.Roles)
.SelectMany(x => x.Employees)
.Distinct()
.SelectMany(x => x.PurchaseOrders)
.Select(po => new PurchaseOrderViewModel
{
Name = po.PONo,
Id = po.Id
})).ToList();
var finalList = poList1.Union(poList2);
I can't find the correct way to build an EF (4.1) query that will return the same result as this SQL containing a LEFT JOIN:
SELECT
s.id_service,
s.description,
x.id_service as isDisponible
FROM
role.service_disponible s
LEFT JOIN
role.service_disponible_x_ue x
ON s.id_service = x.id_service AND x.id_ue = 1 and flg_actif = '1'
In fact I'm just trying to obtain the complete list of services disponible (ServiceDisponible) adding a field that tell me if service is disponible for a specific entity (filtered with the id_ue) which information come from a many to many related table (ServiceDisponibleXUe).
My model is:
Ideally, I would like this query to return this viewModel object what is basically my serviceDisponible domain with one more field indicating the disponibility of the service.
public ServiceDisponibleViewModel(ServiceDisponible ServiceDisponible, bool isDisponible)
{
this.serviceDisponible = serviceDisponible;
this.isDisponible = isDisponible;
}
What I have so far is this query but the syntax is invalid:
services = context.ServiceDisponible
.Select(a => new ServiceDisponibleViewModel
{
c => new ServiceDisponible
{
id_service = a.id_service,
description = a.description
},
isDisponible = a.ServiceDisponibleXUe
.Any(b => b.flg_actif && b.id_ue == idUe)
}).ToList();
Try this:
ServiceDisponibleViewModel services =
from sd in context.ServiceDisponible
from sdx in context.ServiceDisponibleXUe
.Where(x => x.id_ue == 1 && flg_actif == '1' && x.id_service == sd.id_service)
.DefaultIfEmpty()
select new ServiceDisponibleViewModel(
new ServiceDisponible
{
id_service = sd.id_service,
description = sd.description
},
sdx.id_service
);
Having SQL as example often makes one jump to a join in linq. But using navigation properties produces much more succinct syntax:
from sd in context.ServiceDisponible
from sdx in sd.ServiceDisponibleXUes.Where(x => x.id_ue == 1
&& x.flg_actif == "1")
.DefaultIfEmpty()
select new
{ sd.id_service,
sd.description,
isDisponible = sdx.id_service
};
(I couldn't help using the plural form of ServiceDisponibleXUe which imo is more clear).