How to search a text using entity framework after split? - c#

I have a entity model
public class User{
public string FirstName {get;set;}
public string LastName {get;set;}
public string Department {get;set;}
}
So I want to search a text like "john smith" in database using entity framework core 3.1.
I am splitting the text before.
public async Task<IEnumerable<UserListViewModel>> Search(string search)
{
var terms = search.Split(" ");
var queryable = _context.Users.Where(s => terms.All(m => s.Department.ToLower().Contains(m)) ||
terms.All(m => s.FirstName.ToLower().Contains(m)) ||
terms.All(m => s.LastName.ToLower().Contains(m))).AsQueryable();
...........
...........
...........
}
but does it not work.
So how can I do it?

EF Core 3.x doesn't really support translation of All and Any in most cases, and your code is slightly wrong, I think what you really want is:
var queryable = _context.Users.Where(u => terms.All(m => u.Department.Contains(m) ||
u.FirstName.Contains(m) ||
u.LastName.Contains(m)));
Since this can't be translated, you need to reformat it into code that can.
With LINQKit you can use PredicateBuilder to create an extension that will remap the query into a series of && tests for each term:
// searchTerms - IEnumerable<TKey> where all must be in a row's key
// testFne(row,searchTerm) - test one of searchTerms against a row
// dbq.Where(r => searchTerms.All(s => testFne(r,s)))
public static IQueryable<T> WhereAll<T,TKey>(this IQueryable<T> dbq, IEnumerable<TKey> searchTerms, Expression<Func<T, TKey, bool>> testFne) {
var pred = PredicateBuilder.New<T>();
foreach (var s in searchTerms)
pred = pred.And(r => testFne.Invoke(r, s));
return dbq.Where((Expression<Func<T,bool>>)pred.Expand());
}
which you would use like:
var queryable = _context.Users
.WhereAll(terms,
(u,m) => u.Department.Contains(m) ||
u.FirstName.Contains(m) ||
u.LastName.Contains(m));
For "john smith", the extension method would create the equivalent of:
var queryable = _context.Users
.Where(u => (u.Department.Contains("john") ||
u.FirstName.Contains("john") ||
u.LastName.Contains("john")) &&
(u.Department.Contains("smith") ||
u.FirstName.Contains("smith") ||
u.LastName.Contains("smith"))
);

How about the following.
void Main()
{
var users = new List<User>
{
new User { FirstName = "John", LastName = "Smith", Department = "Web" },
new User { FirstName = "Aaliyah", LastName = "Lin", Department = "Warehouse" },
new User { FirstName = "Cristian", LastName = "Stone", Department = "Cleaning" },
new User { FirstName = "Kierra", LastName = "Davidson", Department = "Mobile" },
new User { FirstName = "Lizbeth", LastName = "Gregory", Department = "Web" }
};
var search = "Lizbeth Gregory";
var terms = search.ToLower().Split(' ');
users.Where(s => terms.All(m => s.Department.ToLower().Contains(m)) ||
(terms.Any(m => s.FirstName.ToLower().Contains(m))) ||
terms.Any(m => s.LastName.ToLower().Contains(m)))
.Dump();
}
public class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Department { get; set; }
}

I think you don't need terms.All. as "john smith" is a full name all of it is not gonna be found in a first or last name field.
I'm not sure if the following is possible.
var queryable = _context.Users.Where(s => terms.Contains(m => s.Department.ToLower().Contains(m)) &&
terms.Contains(m => s.FirstName.ToLower().Contains(m)) ||
terms.All(m => s.LastName.ToLower().Contains(m))).AsQueryable();
Though it's not quite accurate, it'd return "john john" as well this way, but it's rare.

An alternative solution would be to aggregate your search terms:
var terms = search
.Split(' ', StringSplitOptions.RemoveEmptyEntries);
query = terms
.Aggregate(query, (current, term) =>
current.Where(x =>
x.FirstName.Contains(term)
|| x.LastName.Contains(term)
|| x.Department.Contains(term)
)
);

Related

Conditional filter retrieve partial object

how to retrieve partial object?
{
Id:123,
Name:"david",
Languages:[{b:"en"},{b:"ru"}]
}
public async Task<myObj> Get(long id, string lang=null)
{
FilterDefinition<myObj> filter = Builders<myObj>.Filter.Eq(s => s.Id, id)
& Builders<myObj>.Filter.ElemMatch(l => l.Languages, s => s.b== lang);
ProjectionDefinition<myObj> projection = Builders<Symptom>.Projection
.Include(d => d.Id)
.Include(d => d.Name)
.Include(d => d.Languages[-1]);
FindOptions<myObj> options = new FindOptions<myObj> { Projection = projection };
using (IAsyncCursor<myObj> cursor = await db.Collection.FindAsync(filter, options))
{
return cursor.SingleOrDefault();
}
}
if i call function get(123,"cn") i expect to get:
{
Id:123,
Name:"david",
Languages:null
}
instead of null.
how to fix the query to achieve my demand?
i think this will get the job done:
public async Task<myObj> Get(long id, string lang = null)
{
var res = await db.Collection.AsQueryable()
.Where(m =>
m.Id == id &&
m.Languages.Any(l => l.b == lang))
.SingleOrDefaultAsync();
return res ?? new myObj { _Id = id, Languages = null };
}
If you want to display the languages only when they match (and null if none match up), then try the following
public async Task<myObj> Get(long id, string lang = null)
{
FilterDefinition<myObj> filter = Builders<myObj>.Filter.Eq(s => s.Id, id)
var result = await collection.Find(filter).SingleOrDefaultAsync();
if (result != null)
result.Languages = result.Languages?.Where(lng => lng.b.Equals(lang)).ToList();
return result;
}
You will get your object that you want based on the ID.. then further it will return only those languages that match up with language that you are passing (null or otherwise).
It's working. I don't know what you mean by "instead of null".
One minor thing, that you would like to not include Languges, instead you projected the Languages to an array range with the [-1]. So it's just return last element of the array. The final code is:
> db.ItemWithLanguages.find()
{ "_id" : ObjectId("5dfb57c9692d22eefa6e0cfe"), "Id" : 123, "Name" : "david", "Languages" : [ { "B" : "en" }, { "B" : "cn" } ] }
internal class MyObj
{
public long Id { get; set; }
[BsonId]
[BsonElement("_id")]
public ObjectId MyId { get; set; }
public string Name { get; set; }
public List<Language> Languages { get; set; }
}
internal class Language
{
public string B { get; set; }
}
public static async Task<MyObj> Get(IMongoCollection<MyObj> collection, long id, string lang = null)
{
FilterDefinition<MyObj> filter = Builders<MyObj>.Filter.Eq(s => s.Id, id)
& Builders<MyObj>.Filter.ElemMatch(l => l.Languages, s => s.B == lang);
// excluding d.Languages by not including it.
// it makes Languages = null.
ProjectionDefinition<MyObj> projection = Builders<MyObj>.Projection
.Include(d => d.Id)
.Include(d => d.Name);
FindOptions<MyObj> options = new FindOptions<MyObj> { Projection = projection };
using (IAsyncCursor<MyObj> cursor = await collection.FindAsync(filter, options))
{
return cursor.SingleOrDefault();
}
}
...
string connectionString = "mongodb://localhost:27017";
var client = new MongoClient(connectionString);
var db = client.GetDatabase("test");
var myObjs = db.GetCollection<MyObj>("ItemWithLanguages");
MyObj ret;
Task.Run(async () => { ret = await Get(myObjs, 123, "cn"); }).ConfigureAwait(false).GetAwaiter()
.GetResult();

LINQ To Entities INNER JOIN with COUNT

I'm trying to do replicate the following query in LINQ using the comprehension method. My SQL is as follows:
SELECT COUNT(Post.Id), User.Id, User.Name
FROM Post
INNER JOIN User ON Post.ModeratorId = User.Id
WHERE Post.Status IN ("Live", "Pending", "Expired")
GROUP BY User.Id, User.Name
My LINQ query is as follows but its still returns a 0 count when no moderator has been assigned to the post. Note a Post.ModeratorId is a nullable value.
I only want a list of moderators and a count of post they are or have moderated.
How can I replicate the above SQL in LINQ?
public IEnumerable<ModeratorPostViewModel> GetModeratorPostCount()
{
var posts = _context.Post
.Include(p => p.Moderator)
.Include(p => p.Status)
.Where(p => p.ModeratorId != null && p.Status.Name IN ("Live", "Pending", "Expired"))
.OrderBy(p => p.Moderator.Name)
.Select(p => new ModeratorPostViewModel
{
Id = p.ModeratorId,
Name = p.Moderator.Name,
CountOfPosts = p.Moderator.ModeratorPosts.Count()
})
.ToList();
// Return list
return posts
}
My models are defined as follows:
public class Post
{
int Id { get; set; }
int StatusId { get; set; }
string ModeratorId { get; set; }
// Navigation properties
public Status Status { get; set; }
public ApplicationUser Moderator { get; set; }
// some other other properties
}
public class ApplicationUser : IdentityUser
{
public string Name { get; set; }
// Navigation property
public ICollection<Post> ModeratorPosts { get; set; }
}
I only want a list of moderators and a count of post they are or have moderated
Then base your query on Moderator (or whatever it's called) entity:
var statuses = new [] { "Live", "Pending", "Expired" };
var posts = _context.Moderator
.OrderBy(m => m.Name)
.Select(m => new ModeratorPostViewModel
{
Id = m.Id,
Name = m.Name,
CountOfPosts = m.ModeratorPosts.Count(p => statuses.Contains(p.Status.Name))
})
.Where(m => m.CountOfPosts != 0)
.ToList();
UPDATE: I have to admit that the above LINQ query does not produce a very good SQL, especially with the last Where condition. So you might resort to the exact LINQ equivalent of your SQL query (you missed the GROUP BY part):
var statuses = new [] { "Live", "Pending", "Expired" };
var posts = _context.Post
.Where(p => p.ModeratorId != null && statuses.Contains(p.Status.Name))
.GroupBy(p => new { Id = p.ModeratorId, Name = p.Moderator.Name })
.Select(g => new ModeratorPostViewModel
{
Id = g.Key.Id,
Name = g.Key.Name,
CountOfPosts = g.Count()
})
.OrderBy(m => m.Name)
.ToList();
You can do a group by after filtering the posts by the statuses you are looking for
var s = new List<string>() {"Live", "Pending", "Expired"};
var grouped = db.Post.Where(x => s.Contains(x.Status)) //Fitler for the statuses
.GroupBy(f => f.ModeratorId, po => po,
(k, items) => new ModeratorPostViewModel
{
Name = items.FirstOrDefault().User.Name,
Id=k,
CountOfPosts = items.Count()
}).ToList();

Difference between two List with same entries but not the same count

I encountered an issue while comparing two Lists. I have two Lists of customs objects and I want to have the difference between them and then Count the numbers of results. Here is an example :
This is my custom class :
public class Contact
{
public String FirstName { get; set; }
public String LastName { get; set; }
public bool IsAdmin { get; set; }
}
And In my app when I make a difference between two List It sound like this :
List<Contact> Difference =
List1.Where(Second =>
!List2.Any(First =>
First.FirstName == Second.FirstName
&& First.LastName == Second.LastName
&& First.IsAdmin == Second.IsAdmin))
.ToList();
This method give me the results which match with the condition , so I can Group and count my results except when I have a result like that :
List<Contact> List1 = new List<Contact>
{
new Contact { Firstname = "Tom", LastName = "Smith", IsAdmin = true },
new Contact { Firstname = "Tom", LastName = "Smith", IsAdmin = true },
new Contact { Firstname = "Bob", LastName = "Smith", IsAdmin = false },
new Contact { Firstname = "Vincent", LastName = "Smith", IsAdmin = false }
};
List<Contact> List2 =new List<Contact>
{
new Contact { Firstname = "Tom", LastName = "Smith", IsAdmin = true},
new Contact { Firstname = "Bob", LastName = "Smith", IsAdmin = false}
};
When I run my method I have 1 results :
new Contact { Firstname = "Vincent", LastName = "Smith", IsAdmin = false }
because It matchs with the condition
But I want this as result :
new Contact { Firstname = "Tom", LastName = "Smith", IsAdmin = true}
new Contact { Firstname = "Vincent", LastName = "Smith", IsAdmin = false }
How could you make it possible ?
Edit : Working method :
var groups1 = List1
.GroupBy(c => new { c.Firstname, c.LastName, c.IsAdmin });
var groups2 = List2
.GroupBy(c => new { c.Firstname, c.LastName, c.IsAdmin });
var diffs = from g1 in groups1
join g2 in groups2
on g1.Key equals g2.Key into gj
from g2 in gj.DefaultIfEmpty(Enumerable.Empty<Contact>())
where g1.Count() > g2.Count()
select new { g1, g2 };
List<Contact> allDiffs = diffs
.SelectMany(x => x.g1.Take(x.g1.Count() - x.g2.Count()))
.ToList();
Maybe you want all items that are in list1 but not in list2 even those which are in list2 but not in the same amount, try this:
var groups1 = List1
.GroupBy(c => new { c.Firstname, c.LastName, c.IsAdmin });
var groups2 = List2
.GroupBy(c => new { c.Firstname, c.LastName, c.IsAdmin });
var diffs = from g1 in groups1
join g2 in groups2
on g1.Key equals g2.Key into gj
from g2 in gj.DefaultIfEmpty(Enumerable.Empty<Contact>())
where g1.Count() > g2.Count()
select new { g1, g2 };
List<Contact> allDiffs = diffs
.SelectMany(x => x.g1.Take(x.g1.Count() - x.g2.Count()))
.ToList();
( edit: i hope that there is an easier way but it works )
If you need common item(s) in the both list then remove the negation operator
List<Contact> Difference =
Contact_List2.Where(Second =>
Contact_List1.Any(First =>
First.FirstName == Second.FirstName
&& First.LastName == Second.LastName
&& First.IsAdmin == Second.IsAdmin))
.ToList();
What you really want here is an intersection, there is an Intersect method available with LINQ but for it to work with custom objects you would need to implement IEquatable<T> (or implement your own custom IEqualityComparer<T>).
This would allow you to simply call var difference = List2.Intersect(List1), however, as to the reason why your code currently fails...
List<Contact> Difference = Contact_List2.Where(Second =>
!Contact_List1.Any(First =>
First.FirstName == Second.FirstName
&& First.LastName == Second.LastName
&& First.IsAdmin == Second.IsAdmin))
.ToList();
You are asking for all the records in List2 which aren't in List1 - what you want is all the records in List2 which are also in List1.
To fix, just remove the ! (not) operator from the Any check i.e.
Contact_List2.Where(x => Contact_List1.Any(...));
Alternatively, you could use Enumerable.Intersect with a custom equality comparer. It will produce an IEnumerable<T> of the items that are found in both lists. You would use it this way:
var matching = List1.Intersect(List2, new ContactComparer());
And the custom comparer (shamelessly lifted and adapted from the MSDN docs):
public sealed class ContactComparer : IEqualityComparer<Contact>
{
public bool Equals(Contact x, Contact y)
{
if (Object.ReferenceEquals(x, y)) return true;
if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
return false;
return x.FirstName == y.FirstName &&
x.LastName == y.LastName &&
x.IsAdmin == y.IsAdmin;
}
public int GetHashCode(Contact contact)
{
if (Object.ReferenceEquals(contact, null)) return 0;
//Get hash code for the Name field if it is not null.
int hashContactFirst = contact.FirstName == null ? 0 : contact.FirstName.GetHashCode();
//Get hash code for the Code field.
int hashContactLast = contact.LastName == null ? 0 : contact.LastName.GetHashCode()
int hashAdmin = contact.IsAdmin.GetHashCode();
return hashContactFirst ^ hashContactLast ^ hashAdmin;
}
}
I would personally favour this route because it creates more readable linq that is easier to understand with a quick glance.
I'm not sure how easy it is to do using LINQ alone. I think the below code gives the result you're after. Of course it's actually changing List1 so you'll need to work on a copy if you need the original list intact.
foreach (Contact contact2 in List2)
{
List1.Remove(List1.FirstOrDefault(contact1 => contact1.FirstName == contact2.FirstName
&& contact1.LastName == contact2.LastName
&& contact1.IsAdmin == contact2.IsAdmin));
}

Search based on a set of keywords

I need to make a search based on a set of keywords, that return all the Ads related with those keywords. Then the result is a list of Categories with the Ads Count for each Category.
The search is made in a KeywordSearch Table:
public class KeywordSearch
{
public int Id { get; set; }
public string Name { get; set; }
public Keyword Keyword { get; set; }
}
Where the Keyword Table is:
public class Keyword
{
public int Id { get; set; }
public string Name { get; set; }
}
The Ads are related with the Keywords using the following Table:
public class KeywordAdCategory
{
[Key]
[Column("Keyword_Id", Order = 0)]
public int Keyword_Id { get; set; }
[Key]
[Column("Ad_Id", Order = 1)]
public int Ad_Id { get; set; }
[Key]
[Column("Category_Id", Order = 2)]
public int Category_Id { get; set; }
}
Finally, the Category table:
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
Example:
Keywords: "Mercedes-Benz" and "GLK"
KeywordSearch: "Mercedes" and "Benz" for the Keyword "Mercedes-Benz"
"GLK" for the Keyword "GLK"
Category: "Cars" and "Trucks"
Ads: Car - Mercedes-Benz GLK
Truck - Mercedes-Benz Citan
If I search "Mercedes-Benz" I get:
Cars: 1
Trucks: 1
If I search "Mercedes-Benz GLK" I get:
Cars: 1
If I search "Mercedes Citan" I get:
Trucks: 1
What I get until now:
var keywordIds = from k in keywordSearchQuery
where splitKeywords.Contains(k.Name)
select k.Keyword.Id;
var matchingKac = from kac in keywordAdCategoryQuery
where keywordIds.Distinct().Contains(kac.Keyword_Id)
select kac;
var addIDs = from kac in matchingKac
group kac by kac.Ad_Id into d
where d.Count() == splitKeywords.Count()
select d.Key;
var groupedKac = from kac in keywordAdCategoryQuery
where addIDs.Contains(kac.Ad_Id) <--- EDIT2
group kac by new { kac.Category_Id, kac.Ad_Id };
var result = from grp in groupedKac
group grp by grp.Key.Category_Id into final
join c in categoryQuery on final.Key equals c.Id
select new CategoryGetAllBySearchDto
{
Id = final.Key,
Name = c.Name,
ListController = c.ListController,
ListAction = c.ListAction,
SearchCount = final.Count()
};
The problem is that I can't get only the Ads that match all Keywords.
EDIT:
When a keyword is made of 2 or more KeywordSearches like "Mercedes-Benz", the line "where d.Count() == splitKeywords.Count()" fails, because d.count = 1 and splitkeywords.Count = 2 for "Mercedes-Benz"
Any Help?
this may not be the direct answer, but in such "multiple parameter search" situations i just forget about anything and do the simple thing, for ex: Search By Car Manufacturer, CategoryId, MillageMax, Price :
var searchResults = from c in carDb.Cars
where (c.Manufacturer.Contains(Manufacturer) || Manufacturer == null) &&
(c.CategoryId == CategoryId || CategoryId == null) &&
(c.Millage <= MillageMax || MillageMax== null) &&
(c.Price <= Price || Price == null)
select c
now if any of the parameters is null it cancels the containing line by making the whole expression in brackets True and so it does not take a part in search any more
If you try to make your own search engine you will probably fail.Why don't you try Lucene.
Here's a link http://lucenenet.apache.org/.
Cheers
I think I have a solution now. This is based on your previous question and a few assumptions:
Keywords are complete names like "Mercedes-Benz GLK", "Mercedes-Benz Citan".
KeywordSearchs are "Mercedes", "Benz" and "GLK" for "Mercedes-Benz GLK" and "Mercedes", "Benz" and "Citan" for "Mercedes-Benz Citan"
"Mercedes-Benz GLK" is a "Car", "Mercedes-Benz Citan" is a "Truck"
With those three assumptions in mind I can say that
var keywordIds = from k in keywordSearchQuery
where splitKeywords.Contains(k.Name)
select k.Keyword.Id;
is the culprit and all queries below rely on it. This query will find all keywords that contain any words in your searchstring.
Example: Given searchstring "Mercedes-Benz GLK" will be split into "Mercedes", "Benz" and "GLK". Your query now finds "Mercedes" and "Benz" in both "Mercedes-Benz GLK" and "Mercedes-Benz Citan".
I think it's obvious that you don't want "Mercedes-Benz GLK" to match "Mercedes-Benz Citan".
The solution is to tell the query to match every splitKeywords with any Keywordsearch and return the appropriate Keyword:
var keywordIds = keywordSearchQuery
.GroupBy(k => k.Keyword.Id)
.Where(g => splitKeywords.All(w =>
g.Any(k => k.Name.Contains(w))))
.Select(g => g.Key);
As for addIds changing it to var addIDs = matchingKac.Select(ad => ad.Ad_Id).Distinct(); should do the trick. Or if matchingKac is only needed in addIds then you could change it to
var matchingKac = (from kac in keywordAdCategoryQuery
where keywordIds.Distinct().Contains(kac.Keyword_Id)
select kac.Ad_Id).Distinct();
and remove addIds.
I haven't compile-checked this or anything, so it may require some tweaking, but you're looking for something along these lines.
var matchingKac = keywordIds.Distinct().ToList()
.Aggregate(
keywordAdCategoryQuery.AsQueryable(),
(q, id) => q.Where(kac => kac.Keyword_Id == id));
You're effectively saying, "Start with keywordAdCategoryQuery, and for each keyword add a .Where() condition saying that it must have that keyword in it. You could do the same thing with a for loop if you find Aggregate difficult to read.
I am suggesting you to add regex and omit that special characters and then use Linq for that
So Mercedez-Benz can become Mercedez and benz
I recommend to NOT define keywords to objects that way, because you might search and find too many objects or you'll find possibly nothing. You will always spoil your time when searching. Classify your objects in a way that the users focus is to FIND and not to search.
I have posted my answer to: https://github.com/n074v41l4bl34u/StackOverflow19796132
Feel free to review it.
Here is the most important snippet.
with:
internal class SearchDomain
{
public List<Keyword> Keywords { get; set; }
public List<Category> Categories { get; set; }
public List<KeywordAdCategory> KeywordAdCategories { get; set; }
}
then:
private static char[] keywordPartsSplitter = new char[] { ' ', '-' };
internal static Dictionary<Category, Dictionary<int, List<KeywordAdCategory>>> FromStringInput(string searchPhrase, SearchDomain searchDomain)
{
var identifiedKeywords = searchPhrase
.Split(keywordPartsSplitter);
var knownKeywordParts = identifiedKeywords
.Where
(ik =>
searchDomain
.Keywords
.SelectMany(x => x.GetKeywordParts())
.Any(kp => kp.Equals(ik, StringComparison.InvariantCultureIgnoreCase))
);
var keywordkSearches = knownKeywordParts
.Select((kkp, n) => new KeywordSearch()
{
Id = n,
Name = kkp,
Keyword = searchDomain
.Keywords
.Single
(k =>
k.GetKeywordParts()
.Any(kp => kp.Equals(kkp, StringComparison.InvariantCultureIgnoreCase))
)
});
var relevantKeywords = keywordkSearches
.Select(ks => ks.Keyword)
.Distinct();
var keywordAdCategoriesByCategory = searchDomain.Categories
.GroupJoin
(
searchDomain.KeywordAdCategories,
c => c.Id,
kac => kac.Category_Id,
(c, kac) => new { Category = c, AdKeywordsForCategory = kac }
);
var relevantKeywordAdCategories = keywordAdCategoriesByCategory
.Where
(kacbk =>
relevantKeywords
.All
(rk =>
kacbk
.AdKeywordsForCategory
.Any(kac => kac.Keyword_Id == rk.Id)
)
);
var foundAdsInCategories = relevantKeywordAdCategories
.ToDictionary
(rkac =>
rkac.Category,
rkac => rkac.AdKeywordsForCategory
.GroupBy(g => g.Ad_Id)
.ToDictionary(x => x.Key, x => x.ToList())
);
return foundAdsInCategories;
}
It does exactly what you want however I find something fishy about keywords being divisible to sub-keywords. Than again, maybe it is just the naming.

How to extract part of object initializer?

Problem
I have following repository, which queries db and constructs custom objects:
public class PatientCardRepository
{
public PatientCardRepository(DbSet<PersonModel> people)
{
_people = people;
}
private DbSet<PersonModel> _people;
public IEnumerable<PatientCardObject> GetPatientCardDataWithVisits(int personId)
{
return _people.AsNoTracking()
.Where(person => person.Id == personId)
.Select(person => new PatientCardObject
{
Person = new Person // COMMON PART
{
FirstName = person.FirstName,
LastName = person.LastName,
Addresses = person.Addresses
.Where(address => address.IsCurrent && address.AddressTypeId == AddressType)
.Select(address => new Address
{
City = address.City,
Street = address.Street,
StreetNo = address.StreetNo,
ZipCode = address.ZipCode
}),
},
Visits = person.PatientVisits.Select(visit => new Visit
{
Description = visit.Description,
StartTime = visit.StartTime,
EndTime = visit.EndTime,
})
}).Take(100);
}
public IEnumerable<PatientCardObject> GetPatientCardData(int personId)
{
return _people.AsNoTracking()
.Where(person => person.Id == personId)
.Select(person => new PatientCardObject
{
Person = new Person // COMMON PART
{
FirstName = person.FirstName,
LastName = person.LastName,
Addresses = person.Addresses
.Where(address => address.IsCurrent && address.AddressTypeId == AddressType)
.Select(address => new Address
{
City = address.City,
Street = address.Street,
StreetNo = address.StreetNo,
ZipCode = address.ZipCode
}),
}
}).Take(100);
}
}
I want to extract the COMMON PART (get rid of the copy-paste).
Failed attempts
I have tried following solutions, but all failed:
Changing Select clause to multi-line expression:
public IQueryable<PatientCardObject> GetPatientCardDataWithVisits(int personId)
{
return _people.AsNoTracking()
.Where(person => person.Id == personId)
.Select(person =>
{
var p = new PatientCardObject();
p.Person = CreatePersonFromModel(person);
return p;
});
}
This fails because Select accepts only expression lambdas (multi-lines are not allowed)
Using Inlcudes in the first place, then Select after materialization.
public IEnumerable<PatientCardObject> GetPatientCardDataWithVisits(int personId)
{
var filteredPeople = (IEnumerable)(_people.AsNoTracking()
.Include(person => person.Address)
.Include(person => person.PatientVisits)
.Where(person => person.Id == personId));
return filteredPeople
.Select(person =>
{
var p = new PatientCardObject();
p.Person = CreatePersonFromModel(person);
return p;
}).Take(100);
}
This fails, because it selects too many rows and columns. In this example all addresses for person are selected, not only current (filtering is done after materialization)
So we'll start out by having a method to get all of the information that you need by using the method that selects the most information. From that, we'll modify it to return an IQueryable rather than an item, to allow for deferred execution:
private IQueryable<PatientCardObject> GetPatientCardDataWithVisitsHelper(int personId)
{
return _people.AsNoTracking()
.Where(person => person.Id == personId)
.Select(person => new PatientCardObject
{
Person = new Person // COMMON PART
{
FirstName = person.FirstName,
LastName = person.LastName,
Addresses = person.Addresses
.Where(address => address.IsCurrent && address.AddressTypeId == AddressType)
.Select(address => new Address
{
City = address.City,
Street = address.Street,
StreetNo = address.StreetNo,
ZipCode = address.ZipCode
}),
},
Visits = person.PatientVisits.Select(visit => new Visit
{
Description = visit.Description,
StartTime = visit.StartTime,
EndTime = visit.EndTime,
})
});
}
(This is just GetPatientCardDataWithVisits without the call to First and a different name.)
Then we'll just have two calls to it, one that returns the first item, another than "removes" the unneeded information:
public PatientCardObject GetPatientCardDataWithVisits(int personId)
{
return GetPatientCardDataWithVisitsHelper(personId).First();
}
public PatientCardObject GetPatientCardData(int personId)
{
return GetPatientCardDataWithVisitsHelper(personId)
.Select(person => new PatientCardObject
{
Person = person.Person,
Visits = person.Visits.Where(v => false),
}).First();
}
Your second example has almost got it right. Try this:
public IEnumerable<PatientCardObject> GetPatientCardDataWithVisits(int personId)
{
var filteredPeople = _people.AsNoTracking()
.Include(person => person.Address)
.Include(person => person.PatientVisits)
.Where(person => person.Id == personId)
.Take(100)
.AsEnumerable()
.Select(x => new PatientCardObject { Person = CreatePersonFromModel(x) });
return filteredPeople;
}
This will only retrieve 100 from the DB.

Categories

Resources