linq store select clause as function variable - c#

is there a way to store the select clause of a linq statement as a variable?
I have the following class and the select clause is repeated can I store it as a variable somehow so I don't need to repeat it?
public class TractRepository : ITractRepository
{
private DataContext context;
public TractRepository(DataContext ctx) => context = ctx;
public async Task<IEnumerable<Tract>> GetAllAsync() =>
await context.Tracts.Include(p => p.ContractType).Include(p => p.ContractSubType).OrderByDescending(p => p.Id)
.Select(p => new Tract
{
Id = p.Id,
Acreage = p.Acreage,
Administrative = p.Administrative,
ContractType = new ContractType
{
Id = p.ContractType.Id,
ContractTypeName = p.ContractType.ContractTypeName
},
ContractSubType = new ContractSubType
{
Id = p.ContractSubType.Id,
ContractSubTypeName = p.ContractSubType.ContractSubTypeName
}
})
.ToListAsync();
public async Task<Tract> GetByIdAsync(long id) =>
await context.Tracts.Include(p => p.ContractType).Include(p => p.ContractSubType)
.Select(p => new Tract
{
Id = p.Id,
Acreage = p.Acreage,
Administrative = p.Administrative,
ContractType = new ContractType
{
Id = p.ContractType.Id,
ContractTypeName = p.ContractType.ContractTypeName
},
ContractSubType = new ContractSubType
{
Id = p.ContractSubType.Id,
ContractSubTypeName = p.ContractSubType.ContractSubTypeName
}
}).FirstOrDefaultAsync(p => p.Id == id);
}

You can extract the whole common IQueryable<T> to separate property/method. Since LINQ queries are not executed until enumerated (the so called deferred execution), it can be used as base for composing other queries, e.g.
private IQueryable<Tract> Query() => context.Tracts
//.Include(p => p.ContractType)
//.Include(p => p.ContractSubType)
.Select(p => new Tract
{
Id = p.Id,
Acreage = p.Acreage,
Administrative = p.Administrative,
ContractType = new ContractType
{
Id = p.ContractType.Id,
ContractTypeName = p.ContractType.ContractTypeName
},
ContractSubType = new ContractSubType
{
Id = p.ContractSubType.Id,
ContractSubTypeName = p.ContractSubType.ContractSubTypeName
}
});
(Includes are redundant (ignored) for projection (Select) queries)
then
public async Task<IEnumerable<Tract>> GetAllAsync() =>
await Query().OrderByDescending(p => p.Id).ToListAsync();
and
public async Task<Tract> GetByIdAsync(long id) =>
await Query().FirstOrDefaultAsync(p => p.Id == id);

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();

Getting join results using LINQ to SQL

I'm new to LINQ. I'm trying to join two tables, but I have difficulties returning the results.
Here is my code:
using (DatabaseDataContext db = new DatabaseDataContext())
{
var list = db.Products.Join(db.ProductDetails,
p => p.ID,
d => d.ProductID,
(p, d) => new {
p.ID,p.Photo,d.Name,d.LanguageID
}).Where(d=>d.LanguageID== lang).ToList();
}
Well I can not use the variable list outside using and when I declare the variable outside 'using' (before it) like: var list;.
I get the error:
Implicitly-typed local variables must be initialized
Update:
I changed the code to:
DatabaseDataContext db = new DatabaseDataContext();
var products = db.Products.Join(db.ProductDetails,
p => p.ID,
d => d.ProductID,
(p, d) => new {
p.ID,p.Photo,d.Name,d.LanguageID
}).Where(d=>d.LanguageID== langs[language].ParseInt()).ToList();
and it worked. As I have omitted using, do I have to do anything like closing the connection?
Is there a problem not using the using?
If you don't use the results of the query in the same scope, you must make it typed so you can declare variables of appropriate type. First define a class for the result objects and use it. It would be cleaner to put this all as a method.
public class Result
{
public int ID { get; set; }
public string Photo { get; set; }
public string Name { get; set; }
public int LanguageID { get; set; }
}
public List<Result> GetJoinResult(int languageId)
{
using (DatabaseDataContext db = new DatabaseDataContext())
{
return db.Products.Join(db.ProductDetails, p => p.ID, d => d.ProductID,
(p, d) => new Result // not anonymous
{
ID = p.ID,
Photo = p.Photo,
Name = d.Name,
LanguageID = d.LanguageID,
})
.Where(x => x.LanguageID == languageId)
.ToList();
}
}
If defining the types is too much, then you must use it immediately in the same scope.
using (DatabaseDataContext db = new DatabaseDataContext())
{
var results = db.Products.Join(db.ProductDetails, p => p.ID, d => d.ProductID,
(p, d) => new
{
p.ID,
p.Photo,
d.Name,
d.LanguageID,
})
.Where(x => x.LanguageID == languageId)
.ToList();
// do stuff with results
}

How can I add an index value to a select into a class with LINQ?

I have the following:
var result = await db.TestQuestions
.Where(t => t.TestId == testId)
.Select((t, index) => new GetAllDTO
{
QuestionUId = t.QuestionUId,
QuestionNumber = index
}).ToListAsync();
and:
public class GetAllDTO
{
public Guid QuestionUId { get; set; }
public int QuestionNumber { get; set; }
}
This is giving me an error when I added the setting of the QuestionNumber.
That overloaded version of Select is not supported in Linq to Entities.So you can't use it, instead you can do:
var result = await db.TestQuestions
.Where(t => t.TestId == testId)
.Select(t => new GetAllDTO
{
QuestionUId = t.QuestionUId
}).ToListAsync();
int i = 0;
foreach(var dto in result)
dto.QuestionNumber = i++;
Or, this should also work:
var result = await db.TestQuestions
.Where(t => t.TestId == testId)
.AsEnumerable() // notice the AsEnumerable() call
.Select((t, index) => new GetAllDTO
{
QuestionUId = t.QuestionUId,
QuestionNumber = index
}).ToListAsync();

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