Many thanks to leppie:
Currently I got
Expression<Func<vwMailMerge,bool>> whereClause= null;
List<vwMailMerge> mailMergeItems = null;
int personType = mailMergeSettings.PersonType.ToInteger();
if (personType > 0)
{
whereClause = this.MailMergeWhereClause(whereClause, f => f.MemberTypeId == personType);
}
if (mailMergeSettings.PersonIds != null)
{
var personIds = mailMergeSettings.PersonIds.ToGuidArray();
if (personIds != null && personIds.Length > 0)
{
var personList = personIds.ToList();
whereClause = this.MailMergeWhereClause(whereClause, f => personList.Contains(f.UserId));
}
}
mailMergeItems = this.ObjectContext.vwMailMerges.Where(whereClause).ToList();
private Expression<Func<vwMailMerge, bool>> MailMergeWhereClause(params Expression<Func<vwMailMerge, bool>>[] wheres)
{
if (wheres.Length == 0)
{
return x => true;
}
Expression result;
if (wheres[0] == null)
{
result = wheres[1].Body;
return Expression.Lambda<Func<vwMailMerge, bool>>(result, wheres[1].Parameters);
}
else
{
result = wheres[0].Body;
for (int i = 1; i < wheres.Length; i++)
{
result = Expression.And(result, wheres[i].Body);
}
return Expression.Lambda<Func<vwMailMerge, bool>>(result, wheres[0].Parameters);
}
}
}
When it gets to "mailMergeItems =" it drops and gives error: "The parameter 'f' was not bound in the specified LINQ to Entities query expression."
I've noticed that when checking only for people, or only for membertypeId, it works properly.. but combined the 2nd gives a error on it's "f=>" I think.
You cant use Func, you need to use Expression<Func>.
The + can be done via Expression.And.
Update (not tested):
Expression<Func<vwMailMerge, bool>> whereClause = null;
...
Expression<Func<vwMailMerge, bool>> MailMergeWhereClause(
params Expression<Func<vwMailMerge, bool>>[] wheres)
{
if (wheres.Length == 0) return x => true;
Expression result = wheres[0].Body;
for (int i = 1; i < wheres.Length; i++)
{
//probaby needs a parameter fixup, exercise for reader
result = Expression.And(result, wheres[i].Body);
}
return Expression.Lambda<Func<vwMailMerge,bool>>(result, wheres[0].Parameters);
}
Update 2:
The above approach fails as I expected. It might be easy to solve on .NET 4 using the ExpressionVistor class. For .NET 3.5 (or if aforementioned is too hard) the following should work.
The approach is the append the where clauses in the IQueryable directly so you end up with:
somequery.Where(x => x.foo).Where(x => x.bar).Where(x => x.baz)
IOW, you can just add them as required, but it will require some changes to the logic/flow of the code you pasted.
You could reformat your question better with the code tool.
However it looks like you could approach the problem in this way to avoid all those func expressions floating around:
this.ObjectContext.vwMailMerges.Where(mm=>IsValidMailMerge(mm,personType)).ToList()
private bool IsValidMailMerge(YourType mailmerge, YourType2 personType)
{
if(...) // type specific criteria here
return true;
else
return false;
}
Related
I have a ViewModel that contains different elements inside different tables that I tend to assign to it by query.
My problem is that I can't do this with IEnumerable (in GetAll() below), it keeps returning me null for RoomCode but for a single item (in GetDeviceId() below) then it works fine.
public IEnumerable<DeviceViewModel> GetAll()
{
var result = deviceRepository.GetAll().Select(x => x.ToViewModel<DeviceViewModel>());
for(int i = 0; i < result.Count(); i++)
{
int? deviceID = result.ElementAt(i).DeviceId;
result.ElementAt(i).RoomCode = deviceRepository.GetRoomCode(deviceID);
}
return result;
}
public DeviceViewModel GetDeviceID(int deviceID)
{
var result = new DeviceViewModel();
var device = deviceRepository.Find(deviceID);
if (device != null)
{
result = device.ToViewModel<DeviceViewModel>();
result.RoomCode = deviceRepository.GetRoomCode(deviceID);
}
else
{
throw new BaseException(ErrorMessages.DEVICE_LIST_EMPTY);
}
return result;
}
public string GetRoomCode(int? deviceID)
{
string roomCode;
var roomDevice = dbContext.Set<RoomDevice>().FirstOrDefault(x => x.DeviceId == deviceID && x.IsActive == true);
if (roomDevice != null)
{
var room = dbContext.Set<Room>().Find(roomDevice.RoomId);
roomCode = room.RoomCode;
}
else
{
roomCode = "";
}
return roomCode;
}
First, you need to materialize the query to a collection in local memory. Otherwise, the ElementAt(i) will query the db and give back some kind of temporary object each time it is used, discarding any change you do.
var result = deviceRepository.GetAll()
.Select(x => x.ToViewModel<DeviceViewModel>())
.ToList(); // this will materialize the query to a list in memory
// Now modifications of elements in the result IEnumerable will be persisted.
You can then go on with the rest of the code.
Second (and probably optional), I also recommend for clarity to use foreach to enumerate the elements. That's the C# idiomatic way to loop through an IEnumerable:
foreach (var element in result)
{
int? deviceID = element.DeviceId;
element.RoomCode = deviceRepository.GetRoomCode(deviceID);
}
I'm working on a project and implemented a search by multiple fields in MVC, using LINQ like so:
public ActionResult SearchResult(SearchViewModel model)
{
List<Requisition> emptyList = new List<Requisition>();
if (model.RequisitionID > 0
|| model.Department > 0
|| model.Status > 0
|| model.RequisitionedBy != null)
{
var results = db.Requisitions.Where(x => x.RequisitionId > 0);
results = ProcessSearchInput(model, results);
return PartialView(results.ToList());
}
return PartialView(emptyList);
}
Helper:
private static IQueryable<Requisition> ProcessSearchInput(SearchViewModel model, IQueryable<Requisition> results)
{
if (model.Department > 0)
results = results.Where(x => x.Department == model.Department);
if (model.RequisitionedBy != null)
results = results.Where(x => x.Requisitioned_By.Contains(model.RequisitionedBy));
if (model.Status > 0)
results = results.Where(x => x.Status.Contains(model.Status.ToString()));
return results;
}
This code works fine.
However, if I add an extra search field to the form, I would also need to add a separate if statement in the controller.
With the current approach, the ProcessSearchInput method will contain too many if statements.
Is there a better way to handle a search with multiple fields?
Your current approach violates the open closed principle. The solution is to create a dynamic filter like in this example. However that is a complicated solution that is worth only if you are going to add more and more filters along the way. If not then don't bother.
I agree with previous comments: your current solution is probably the way to go.
In the real world, you'll soon have to implement filters like 'all customers having either a billing- or a shipping-address in New York', and more complicated stuff. By then, all clever generic stuff will just be in the way.
However, if you promise never to use this in production code:
you can save a lot of typing by using a query by example, where you specify the filter as an instance of the type your source contains:
var example = new Requisition { Department = 8, Requisitioned_By ="john" };
var result = db.Requisitions.FilterByExample(example);
This is a simple implementation:
public static class FilterByExampleHelper
{
public static IQueryable<T> FilterByExample<T>(this IQueryable<T> source, T example) where T : class
{
foreach (var property in typeof(T).GetProperties(BindingFlags.Public|BindingFlags.Instance).Where(p => p.CanRead))
{
ConstantExpression valueEx = null;
var propertyType = property.PropertyType;
if (propertyType.IsValueType)
{
var value = property.GetValue(example);
if (value != null &&
!value.Equals(Activator.CreateInstance(propertyType)))
{
valueEx = Expression.Constant(value, propertyType);
}
}
if (propertyType == typeof(string))
{
var value = property.GetValue(example) as string;
if (!string.IsNullOrEmpty(value))
{
valueEx = Expression.Constant(value);
}
}
if (valueEx == null)
{
continue;
}
var parameterEx = Expression.Parameter(typeof(T));
var propertyEx = Expression.Property(parameterEx, property);
var equalsEx = Expression.Equal(propertyEx, valueEx);
var lambdaEx = Expression.Lambda(equalsEx, parameterEx) as Expression<Func<T, bool>>;
source = source.Where(lambdaEx);
}
return source;
}
}
I have 2 versions of codes for class method "GetUserRoles". Version 1 always works OK while Version 2 is not working if passed-in argument "exludeRoleNames" for that method is absent and exception "Null object reference" is thrown as a result. I would like to understand why Version 2 codes are not always working, and why Version 1 codes require building predicate with "if" and "else" blocks. Thank you in advance.
Version 1:
The following codes are always working. You could notice that there are separate "if" and "else" blocks to build local variable Expression<Func<UserRoleInApplication, bool>> predicate
public List<UserInRoleViewModel> GetUserRoles(long userId, params string[] exludeRoleNames)
{
List<UserInRoleViewModel> results = null;
IQueryable<UserInRoleViewModel> items = null;
Expression<Func<UserRoleInApplication, bool>> predicate = null;
if (exludeRoleNames.Count() <= 0)
{
predicate = x => true;
}
else
{
predicate = x => !exludeRoleNames.Contains(x.UserRole.RoleName);
}
items = from uir in _repository.GetQuery<UserInRole>(x => x.UserId == userId)
join ura in _repository.GetQuery<UserRoleInApplication>(predicate)
on uir.UserRoleInApplicationId equals ura.UserRoleInApplicationId
into g
from item in g
select new UserInRoleViewModel
{
UserInRoleId = uir.UserInRoleId,
UserId = uir.UserId,
UserRoleInApplicationId = uir.UserRoleInApplicationId
};
if (items != null && items.Any())
{
results = new List<UserInRoleViewModel>();
results = items.ToList();
}
return results;
}
Version 2:
However, the following codes throw exception "Null object reference" when calling method GetUserRoles such as GetUserRoles(long userId) without passed-in argument "exludeRoleNames". You could notice that there are no separate "if" and "else" blocks for the codes to form local variable Expression<Func<UserRoleInApplication, bool>> predicate:
public List<UserInRoleViewModel> GetUserRoles(long userId, params string[] exludeRoleNames)
{
List<UserInRoleViewModel> results = null;
IQueryable<UserInRoleViewModel> items = null;
// Note: one line code and no separate "if" and "else" blocks *************
Expression<Func<UserRoleInApplication, bool>> predicate = x => exludeRoleNames.Count() <= 0 ? true : !exludeRoleNames.Contains(x.UserRole.RoleName);
items = from uir in _repository.GetQuery<UserInRole>(x => x.UserId == userId)
join ura in _repository.GetQuery<UserRoleInApplication>(predicate)
on uir.UserRoleInApplicationId equals ura.UserRoleInApplicationId
into g
from item in g
select new UserInRoleViewModel
{
UserInRoleId = uir.UserInRoleId,
UserId = uir.UserId,
UserRoleInApplicationId = uir.UserRoleInApplicationId
};
if (items != null && items.Any()) // Note: throw exception "Null object reference" if parameter "exludeRoleNames" is absent on calling method GetUserRoles such as GetUserRoles(long userId);
{
results = new List<UserInRoleViewModel>();
results = items.ToList();
}
return results;
}
Try changing this:
Expression<Func<UserRoleInApplication, bool>> predicate = x => exludeRoleNames.Count() <= 0 ? true : !exludeRoleNames.Contains(x.UserRole.RoleName);
To this:
Expression<Func<UserRoleInApplication, bool>> predicate = x => true;
if(exludeRoleNames != null)
{
foreach(string exl in exludeRoleNames)
{
string temp = exl;
predicate = predicate.Or(x=>x.UserRole.RoleName == temp);
}
}
The problem is that you're trying to call Count() on exludeRoleNames - which is null. So, rather than check Count(), compare it to null. If it is null, then you can treat it as an empty array. If it isn't null, then check it's contents.
The other problem is that you can't use string[].Contains in a query context. Entity Framework (which I assume you are using) doesn't support that. So, you have to build out the predicate.
I re-write the following codes and they are working. In case of absent parameter "exludeRoleNames", the codes creat an empty string array:
public List<UserInRoleViewModel> GetUserRoles(long userId, params string[] exludeRoleNames)
{
List<UserInRoleViewModel> results = null;
IQueryable<UserInRoleViewModel> items = null;
exludeRoleNames = !exludeRoleNames.Any() ? new string[] { } : exludeRoleNames; // a MUST
Expression<Func<UserRoleInApplication, bool>> predicate = x => !exludeRoleNames.Contains(x.UserRole.RoleName);
items = from uir in _repository.GetQuery<UserInRole>(x => x.UserId == userId)
join ura in _repository.GetQuery<UserRoleInApplication>(predicate)
on uir.UserRoleInApplicationId equals ura.UserRoleInApplicationId
into g
from item in g
select new UserInRoleViewModel
{
UserInRoleId = uir.UserInRoleId,
UserId = uir.UserId,
UserRoleInApplicationId = uir.UserRoleInApplicationId
};
if (items != null && items.Any())
{
results = new List<UserInRoleViewModel>();
results = items.ToList();
}
return results;
}
I have one problem. If i'm using LinqToSql, my program load my database in memory.
little example:
//pageNumber = 1; pageSize = 100;
var result =
(
from a in db.Stats.AsEnumerable()
where (DictionaryFilter(a, sourceDictionary) && DateFilter(a, beginTime, endTime) && ErrorFilter(a, WarnLevel))
select a
);
var size = result.Count(); // size = 1007
var resultList = result.Skip((pageNumber-1)*pageSize).Take(pageSize).ToList();
return resultList;
DictionaryFilter, DateFilter and ErrorFilter are functions that filter my datebase.
after this my program use ~250Mb of Ram.
if i dont use:
var size = result.Count();
My program use ~120MB Ram.
Before use this code, my program use ~35MB Ram.
How can I use count and take functions not loading all my datebase in memory?
static bool DateFilter(Stat table, DateTime begin, DateTime end)
{
if ((table.RecordTime >= begin.ToFileTime()) && (table.RecordTime <= end.ToFileTime()))
{
return true;
}
return false;
}
static bool ErrorFilter(Stat table, bool[] WarnLevel)
{
if (WarnLevel[table.WarnLevel]) return true;
else return false;
}
static bool DictionaryFilter(Stat table, Dictionary<GetSourcesNameResult, bool> sourceDictionary)
{
foreach (var word in sourceDictionary)
{
if (table.SourceName == word.Key.SourceName)
{
return word.Value;
}
}
//
return false;
}
Simple: don't use .AsEnumerable(). That means "switch to LINQ-to-Objects". Before that, db.Stats was IQueryable<T>, which is a composable API, and would do what you expect.
That, however, means that you can't use C# methods like DictionaryFilter and DateFilter, and must instead compose things in terms of the Expression API. If you can illustrate what they do I can probably advise further.
With your edit, the filtering can be tweaked, for example:
static IQueryable<Stat> ErrorFilter(IQueryable<Stat> source, bool[] WarnLevel) {
// extract the enabled indices (match to values)
int[] levels = WarnLevel.Select((val, index) => new { val, index })
.Where(pair => pair.val)
.Select(pair => pair.index).ToArray();
switch(levels.Length)
{
case 0:
return source.Where(x => false);
case 1:
int level = levels[0];
return source.Where(x => x.WarnLevel == level);
case 2:
int level0 = levels[0], level1 = levels[1];
return source.Where(
x => x.WarnLevel == level0 || x.WarnLevel == level1);
default:
return source.Where(x => levels.Contains(x.WarnLevel));
}
}
the date filter is simpler:
static IQueryable<Stat> DateFilter(IQueryable<Stat> source,
DateTime begin, DateTime end)
{
var from = begin.ToFileTime(), to = end.ToFileTime();
return source.Where(table => table.RecordTime >= from
&& table.RecordTime <= to);
}
and the dictionary is a bit like the levels:
static IQueryable<Stat> DictionaryFilter(IQueryable<Stat> source,
Dictionary<GetSourcesNameResult, bool> sourceDictionary)
{
var words = (from word in sourceDictionary
where word.Value
select word.Key.SourceName).ToArray();
switch (words.Length)
{
case 0:
return source.Where(x => false);
case 1:
string word = words[0];
return source.Where(x => x.SourceName == word);
case 2:
string word0 = words[0], word1 = words[1];
return source.Where(
x => x.SourceName == word0 || x.SourceName == word1);
default:
return source.Where(x => words.Contains(x.SourceName));
}
}
and:
IQueryable<Stat> result = db.Stats;
result = ErrorFilter(result, WarnLevel);
result = DateFiter(result, beginTime, endTime);
result = DictionaryFilter(result, sourceDictionary);
// etc - note we're *composing* a filter here
var size = result.Count(); // size = 1007
var resultList = result.Skip((pageNumber-1)*pageSize).Take(pageSize).ToList();
return resultList;
The point is we're now using IQueryable<T> and Expression exclusively.
The following SO question might explain things: Understanding .AsEnumerable in Linq To Sql
.AsEnumerable() loads the entire table.
There is a sample application called MVCCrud. This example is quite good and I would like to use it as the framework on a project that I am working on.
The problem is that MVCCrud uses LingToSQL and I would like to use LinqToEntities. I got most everything to work correctly once I converted over to LinqToEntities except one place.
In the following code on the lines i = typeof(TModel).GetProperty(primaryKey).GetValue(p, null),
cell = getCells(p)
it gives a Linq to Entities does not recognize GetValue.
Can someone help me refactor the following code?
items = items.OrderBy(string.Format("{0} {1}", sidx, sord)).Skip(pageIndex * pageSize).Take(pageSize).AsQueryable();
// Generate JSON
var jsonData =
new
{
total = totalPages,
page,
records = totalRecords,
rows = items.Select(
p => new
{
// id column from repository
i = typeof(TModel).GetProperty(primaryKey).GetValue(p, null),
cell = getCells(p)
}).ToArray()
};
return Json(jsonData);
and here is the getCell method:
private string[] getCells(TModel p)
{
List<string> result = new List<string>();
string a = actionCell(p);
if (a != null)
{
result.Add(a);
}
foreach (string column in data_rows.Select(r => r.value))
{
try
{
// hack for tblcategory.name
string[] parts = column.Split('.');
// Set first part
PropertyInfo c = typeof(TModel).GetProperty(parts[0]);
object tmp = c.GetValue(p, null);
// loop through if there is more than one depth to the . eg tblCategory.name
for (int j = 1; j < parts.Length; j++)
{
c = tmp.GetType().GetProperty(parts[j]);
tmp = c.GetValue(tmp, null);
}
if (tmp.GetType() == typeof(DateTime))
{
result.Add(((DateTime)tmp).ToString(dateTimeFormat));
}
else if (tmp.GetType() == typeof(float))
{
result.Add(((float)tmp).ToString(decimalFormat));
}
else if (tmp.GetType() == typeof(double))
{
result.Add(((double)tmp).ToString(decimalFormat));
}
else if (tmp.GetType() == typeof(decimal))
{
result.Add(((decimal)tmp).ToString(decimalFormat));
}
else
{
result.Add(tmp.ToString());
}
}
catch (Exception)
{
result.Add(string.Empty);
}
}
return result.ToArray();
}
Do this ToList() instead of AsQueryable():
items = items.OrderBy(string.Format("{0} {1}", sidx, sord)).Skip(pageIndex * pageSize).Take(pageSize).ToList();
You can't execute any external method "within" linq query.
And may you say that was working in Linq2Sql then you should know when you call any external method "Like ToString()" Linq2Sql will fetch all data from database then handle your query in the memory and that maybe a serious harming if you have a lot of records.
For more information look at this