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;
}
}
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);
}
This question already has answers here:
Comparing object properties in c# [closed]
(20 answers)
Closed 4 years ago.
I have a database containing components with about 20 properties. To find out if an update is needed I want to check if all properties for the two objects, except DateCreated and Id, matches.
If all matches no update, if not, update db.
Component comp_InApp = new Component()
{
Id = null,
Description = "Commponent",
Price = 100,
DateCreated = "2019-01-30",
// Twenty more prop
};
Component comp_InDb = new Component()
{
Id = 1,
Description = "Component",
Price = 100,
DateCreated = "2019-01-01",
// Twenty more prop
};
// Check if all properties match, except DateCreated and Id.
if (comp_InApp.Description == comp_InDb.Description &&
comp_InApp.Price == comp_InDb.Price
// Twenty more prop
)
{
// Everything up to date.
}
else
{
// Update db.
}
This works, but it's not a very clean way with 20 properties. Is there a better way of achieiving the same result in a cleaner way?
I am using DeepEqual when I don't want/don't have the time to write myself Equals and GetHashCode methods.
You can install it simply from NuGet with:
Install-Package DeepEqual
and use it like:
if (comp_InApp.IsDeepEqual(comp_InDb))
{
// Everything up to date.
}
else
{
// Update db.
}
But keep in mind that this will only work for your case when you want to explicitly compare objects, but not for the case when you want to remove an object form a List or cases like this, when Equals and GetHashCode are invoked.
One way, create a class that implements IEqualityComparer<Component> to encapsulate this logic and to avoid that you have modify the class Comparer itself(if you don't want this Equals logic all time). Then you can use it for a simple Equals of two instances of Component and even for all LINQ methods that accepts it as additional argument.
class ComponentComparer : IEqualityComparer<Component>
{
public bool Equals(Component x, Component y)
{
if (object.ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
return x.Price == y.Price && x.Description == y.Description;
}
public int GetHashCode(Component obj)
{
unchecked
{
int hash = 17;
hash = hash * 23 + obj.Price.GetHashCode();
hash = hash * 23 + obj.Description?.GetHashCode() ?? 0;
// ...
return hash;
}
}
}
Your simple use-case:
var comparer = new ComponentComparer();
bool equal = comparer.Equals(comp_InApp, comp_InDb);
It works also if you have two collections and want to know the difference, for example:
IEnumerable<Component> missingInDb = inAppList.Except( inDbList, comparer );
Here is a solution with Reflection:
static bool AreTwoEqual(Component inApp, Component inDb)
{
string[] propertiesToExclude = new string[] { "DateCreated", "Id" };
PropertyInfo[] propertyInfos = typeof(Component).GetProperties()
.Where(x => !propertiesToExclude.Contains(x.Name))
.ToArray();
foreach (PropertyInfo propertyInfo in propertyInfos)
{
bool areSame = inApp.GetType().GetProperty(propertyInfo.Name).GetValue(inApp, null).Equals(inDb.GetType().GetProperty(propertyInfo.Name).GetValue(inDb, null));
if (!areSame)
{
return false;
}
}
return true;
}
You can use a Reflection but it may slow your application. An alternative way of creating that comparator is to generate it with Linq Expressions. Try this code:
public static Expression<Func<T, T, bool>> CreateAreEqualExpression<T>(params string[] toExclude)
{
var type = typeof(T);
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => !toExclude.Contains(p.Name))
.ToArray();
var p1 = Expression.Parameter(type, "p1");
var p2 = Expression.Parameter(type, "p2");
Expression body = null;
foreach (var property in props)
{
var pare = Expression.Equal(
Expression.PropertyOrField(p1, property.Name),
Expression.PropertyOrField(p2, property.Name)
);
body = body == null ? pare : Expression.AndAlso(body, pare);
}
if (body == null) // all properties are excluded
body = Expression.Constant(true);
var lambda = Expression.Lambda<Func<T, T, bool>>(body, p1, p2);
return lambda;
}
it will generate an expression that looks like
(Component p1, Component p2) => ((p1.Description == p2.Description) && (p1.Price == p2.Price))
Usage is simple
var comporator = CreateAreEqualExpression<Component>("Id", "DateCreated")
.Compile(); // save compiled comparator somewhere to use it again later
var areEqual = comporator(comp_InApp, comp_InDb);
EDIT: to make it more type safe you can exclude properties using lambdas
public static Expression<Func<T, T, bool>> CreateAreEqualExpression<T>(
params Expression<Func<T, object>>[] toExclude)
{
var exclude = toExclude
.Select(e =>
{
// for properties that is value types (int, DateTime and so on)
var name = ((e.Body as UnaryExpression)?.Operand as MemberExpression)?.Member.Name;
if (name != null)
return name;
// for properties that is reference type
return (e.Body as MemberExpression)?.Member.Name;
})
.Where(n => n != null)
.Distinct()
.ToArray();
var type = typeof(T);
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => !exclude.Contains(p.Name))
.ToArray();
/* rest of code is unchanged */
}
Now when using it we have an IntelliSense support:
var comparator = CreateAreEqualExpression<Component>(
c => c.Id,
c => c.DateCreated)
.Compile();
I have a data structure where Modules contain Units and Units contain Sections, and from a list of modules, I want to find the first module that contains at least one unit that contains at least one section, and I want to do something with the module, unit and section.
I initially tried to use modules.Find() for this, but it only tells me what the first non-empty Module is, so I'd have to lookup the Unit twice:
var module = modules.Find(m => m.Units.Exists(u => u.Sections.Count > 0));
if (module == null)
{
throw new Exception("there are no non-empty modules");
}
var unit = module.Units.Find(u => u.Sections.Count > 0);
var section = unit.Sections.First();
doSomeStuff(module, unit, section);
I eventually wrote my own function to do this:
private Tuple<Module, Unit, Section> getFirstModuleWithVisibleSection(List<Module> modules)
{
foreach (var module in modules)
{
foreach (var unit in module.Units)
{
var section = unit.Sections.FirstOrDefault();
if (section != null)
{
return new Tuple<Module, Unit, Section>(module, unit, section);
}
}
}
return null;
}
...
var res = getFirstModuleWithVisibleSection(modules);
if (res == null)
{
throw new Exception("no visible modules");
}
var module = res.Item1;
var unit = res.Item2;
var section = res.Item3;
doSomething(module, unit, section);
This is efficient but it's way more verbose than I was hoping for.
I'm more used to OCaml, where I would use List.find_map, which is like find, except instead of returning true/false you return null or not-null, and it returns the first not-null. In C# it would look something like this:
var (module, unit, section) =
modules.FindMap(module =>
module.Units.FindMap(unit =>
{
var section = unit.Sections.FirstOrDefault();
if (section == null)
{
return null;
}
return (module, unit, section);
}));
Is there a way to do this in C#?
What about:
var query = from m in modules
from u in m.Units
let s = u.Sections.FirstOrDefault()
where s != null
select new
{
m,
u,
s
};
var item = query.FirstOrDefault();
Certainly not elegant but it may meet the need.
public Module FirstModuleWithAUnitWithASection(IEnumerable<Module> modules)
=> modules.Where(module => module.Units != null)
.Select(module => module.Units.Where(unit => unit.Sections != null)
.Select(unit => unit.Sections.Select(section => module)
.First()).First()).First();
I am trying to convert this Func to use string values using linq.dynamic.
Currently I have
Func<IQueryable<Customer>, IOrderedQueryable<Customer>> orderBy = o => o.OrderBy(c => c.Postcode);
But I want to do
string sortItem = "customer";
string order = "ASC"
Func<IQueryable<Customer>, IOrderedQueryable<Customer>> orderBy = o => o.OrderBy(sortItem + " " + order);
I am using the Linq.Dynamic library but I am unable to get it to work with the function.
Any help...
Like the other answer suggests, this may not be possible. However, I wanted to post some code which I wrote recently to do something similar:
// single column sorting support
var sortColumnIndex = Convert.ToInt32(Request["iSortCol_0"]);
Func<LegalComplianceDatatable, string> orderingFunction = (c => sortColumnIndex == 0 ? c.HomeCountry :
sortColumnIndex == 1 ? c.HostCountry :
sortColumnIndex == 2 ? c.YearOneRate :
sortColumnIndex == 3 ? c.YearOtherRate :
sortColumnIndex == 4 ? c.RateType :
c.HostCountry);
if (Request["sSortDir_0"] == "desc")
{
filteredResults = filteredResults.OrderByDescending(orderingFunction);
}
else
{
filteredResults = filteredResults.OrderBy(orderingFunction);
}
I haven't used Linq.Dynamic but you can achieve this if you are comfortable building your own expression tree. For example:
public static class IQueryableExtension
{
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> query, string propertyName)
{
var memberProp = typeof(T).GetProperty(propertyName);
var method = typeof(IQueryableExtension).GetMethod("OrderByInternal")
.MakeGenericMethod(typeof(T), memberProp.PropertyType);
return (IOrderedQueryable<T>)method.Invoke(null, new object[] { query, memberProp });
}
public static IOrderedQueryable<T> OrderByInternal<T, TProp>(IQueryable<T> query, PropertyInfo memberProperty)
{
if (memberProperty.PropertyType != typeof(TProp)) throw new Exception();
var thisArg = Expression.Parameter(typeof(T));
var lamba = Expression.Lambda<Func<T, TProp>>(Expression.Property(thisArg, memberProperty), thisArg);
return query.OrderBy(lamba);
}
}
And you can use this like:
IQueryable<Customer> query; // Some query
query = query.OrderBy("Name"); // Will return an IOrderedQueryable<Customer>
This is the logic for ascending sort, without checking (you will need to make sure the property exists, and so on) and some things can be optimized (the reflected method invocation for example). It should get you started.
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;
}