I have a linq query in which I need to be able to select an variable number of fields from a datatable. I do know all of the fields that could be included, but only two will for sure be in the datatable. I also will know which fields are included in the datatable (it will just be different depending on the user's selections). Right now I set up something like this:
var query = from item in dt.AsEnumerable()
group item by item.Field<string>("ID") into g
select new
{
ID = g.Key, //required
Status = g.Min(i => dostuff(i,"Status")), //not required
Disc = g.Min(i => dostuff(i,"Disc")), //not required
Loc = String.Join<string>(",", from i in g select i.Field<string>("Loc")) //required
};
dostuff(DataRow i,string field)
{
try
{
return i.Field<string>(field);
}
catch
{
return null;
}
}
So dostuff basically is just checking whether or not that field exists in the dataset, and then I would just need to ignore the non-existant fields when working with the query results, which would not be too difficult. However, it seems like there is probably a better way to do this, but I've had a tough time finding anything via Google about using a dynamic select clause.
You could do it with dynamic type (nb, I did not test so this might have typos.):
var query =dt.AsEnumerable().GroupBy(item => item.Field<string>("ID"))
.Select(g => {
dynamic t = new System.Dynamic.ExpandoObject();
if (g.Table.Columns.Any(c => c.ColumnName == "Status"))
t.Status = g.Field<string>("Status");
if (g.Table.Columns.Any(c => c.ColumnName == "Disc"))
t.Disc = g.Field<string>("Disc");
t.ID = g.Key;
t.Loc = String.Join<string>(",",g.Select(i => i.Field<string>("Loc")));
return t;
}
Related
Sorry for strange title of the question, but I don't know how to formulate it more short. If you know how to formulate it better, I will be glad if you edit my question.
So, I have the following table:
I'm tolking about CustomerId and EventType fields. The rest is not important. I think you understand that this table is something like log by customers events. Some customer make event - I have event in the table. Simple.
I need to choice all customers events where each customer had event with type registration and type deposit. In other words, customer had registration before? The same customer had deposit? If yes and yes - I need to select all events of this customer.
How I can do that with the help of LINQ?
So I can write SQL like
select *
From "CustomerEvents"
where "CustomerId" in (
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'deposit'
intersect
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'registration'
)
It works, but how to write it on LINQ?
And second question. SQL above works, but not it is not universal. What if tomorrow I will need to show events of customers who have registration, deposit and - new one event - visit? I have to write new one query. Like:
select *
From "CustomerEvents"
where "CustomerId" in (
select "CustomerId"
from "CustomerEvents"
where "EventType" = 'deposit'
intersect
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'registration'
intersect
select distinct "CustomerId"
from "CustomerEvents"
where "EventType" = 'visit'
)
Uncomfortable :(
As source data, I have List with event types. Is there some way to make it dynamically? I mean, I have new one event in the list - I have new one intersect.
P.S I use Postgres and .NET Core 3.1
Update
I pine here a scheme
I haven't tested to see if this will translate to SQL correctly, but if we assume ctx.CustomerEvents is DbSet<CustomerEvent> you could try this:
var targetCustomerIds = ctx
.CustomerEvents
.GroupBy(event => event.CustomerId)
.Where(grouped =>
grouped.Any(event => event.EventType == "deposit")
&& grouped.Any(event => event.EventType == "registration"))
.Select(x => x.Key)
.ToList();
and then select all events for these customers:
var events = ctx.CustomerEvents.Where(event => targetCustomerIds.Contains(event.CustomerId));
To get targetCustomerIds dynamically with a variable number of event types, you could try this:
// for example
var requiredEventTypes = new [] { "deposit", "registration" };
// First group by customer ID
var groupedByCustomerId = ctx
.CustomerEvents
.GroupBy(event => event.CustomerId);
// Then filter out any grouping which doesn't satisfy your condition
var filtered = GetFilteredGroups(groupedByCustomerId, requiredEventTypes);
// Then select the target customer IDs
var targetCustomerIds = filtered.Select(x => x.Key).ToList();
// Finally, select your target events
var events = ctx.CustomerEvents.Where(event =>
targetCustomerIds.Contains(event.CustomerId));
You can define the GetFilteredGroups method like this:
private static IQueryable<IGrouping<int, CustomerEvent>> GetFilteredGroups(
IQueryable<IGrouping<int, CustomerEvent>> grouping,
IEnumerable<string> requiredEventTypes)
{
var result = grouping.Where(x => true);
foreach (var eventType in requiredEventTypes)
{
result = result.Where(x => x.Any(event => event.EventType == eventType));
}
return result;
}
Alternatively, instead of selecting the target customer IDs, you can try to directly select your target events from the filtered groupings:
// ...
// Filter out any grouping which doesn't satisfy your condition
var filtered = GetFilteredGroups(groupedByCustomerId, requiredEventTypes);
// Select your events here
var results = filtered.SelectMany(x => x).Distinct().ToList();
Regarding the inability to translate the query to SQL
Depending on your database size and particularly on the size of CustomerEvents table, this solution may or may not be ideal, but what you could do is load the optimized collection to memory and perform the grouping there:
// for example
var requiredEventTypes = new [] { "deposit", "registration" };
// First group by customer ID, but load into memory
var groupedByCustomerId = ctx
.CustomerEvents
.Where(event => requiredEventTypes.Contains(event.EventType))
.Select(event => new CustomerEventViewModel
{
Id = event.Id,
CustomerId = event.CustomerId,
EventType = event.EventType
})
.GroupBy(event => event.CustomerId)
.AsEnumerable();
// Then filter out any grouping which doesn't satisfy your condition
var filtered = GetFilteredGroups(groupedByCustomerId, requiredEventTypes);
// Then select the target customer IDs
var targetCustomerIds = filtered.Select(x => x.Key).ToList();
// Finally, select your target events
var events = ctx.CustomerEvents.Where(event =>
targetCustomerIds.Contains(event.CustomerId));
You will need to create a type called CustomerEventViewModel like this (so you don't have to load the entire CustomerEvent entity instances to memory):
public class CustomerEventViewModel
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string EventType { get; set; }
}
And change the GetFilteredGroups like this:
private static IEnumerable<IGrouping<int, CustomerEvent>> GetFilteredGroups(
IEnumerable<IGrouping<int, CustomerEvent>> grouping,
IEnumerable<string> requiredEventTypes)
{
var result = grouping.Where(x => true);
foreach (var eventType in requiredEventTypes)
{
result = result.Where(x => x.Any(event => event.EventType == eventType));
}
return result;
}
It should now work fine.
Thank you for #Dejan Janjušević. He is excpirienced developer. But it seems EF can't translate him solution to SQL (or just my hands grow from wrong place). I publish here my solution for this situation. It's simple stuped. So. I have in the table EventType. It is string. And I have from the client the following filter request:
List<string> eventType
Just list with event types. So, in the action I have the following code of the filter:
if (eventType.Any())
{
List<int> ids = new List<int>();
foreach (var e in eventType)
{
var customerIdsList =
_context.customerEvents.Where(x => x.EventType == e).Select(x => x.CustomerId.Value).Distinct().ToList();
if (!ids.Any())
{
ids = customerIdsList;
}
else
{
ids = ids.Intersect(customerIdsList).ToList();
}
}
customerEvents = customerEvents.Where(x => ids.Contains(x.CustomerId.Value));
}
Not very fast, but works.
I am trying to link up the RestaurantId in the RestaurantReservationEventsTbl with the RestaurantID in the RestaurantTbl to display reservations that are only made for the currently logged in restaurant.
I am receiving the following error in my code operator == cannot be applied to operands of type int and iqueryable int
Here is what I am doing in my home controller
var RestaurantIDRestaurantTbl = from r in db.Restaurants select r.RestaurantID;
//var listOfRestaurantsReservations = db.RestaurantReservationEvents.ToList();
var listOfRestaurantsReservations = db.RestaurantReservationEvents.Where(x => x.RestaurantID == RestaurantIDRestaurantTbl).ToList();
//return View(restaurants.Where(x => x.RestaurantEmailAddress == UserEmail).ToList());
//create partial view called _RestaurantReservation
return PartialView("_RestaurantReservations", listOfRestaurantsReservations);
You have to change your code to materialize the restaurantIds like this:
var RestaurantIDRestaurantTbl = (from r in db.Restaurants
select r.RestaurantID).ToList();
Then you may change the code as below for the comparison to work:
var listOfRestaurantsReservations = db.RestaurantReservationEvents.Where(x => RestaurantIDRestaurantTbl.Contains(x.RestaurantID)).ToList();
Anyway this is not the best solution. I will write another example for you, just try this example if it is working or not and let me know for the result.
I would considering changing the code as below to be much more efficient:
var listOfRestaurantsReservations = (from r in db.Restaurants
join e in db.RestaurantReservationEvents
on r.RestaurantID equals e.RestaurantID
//where r.RestaurantID == something //if where condition needed
select e).ToList();
If your tables are not connected with foreignkeys please consider to read this documentation here to make a better structure of the tables since they are related to each-other.
If your tables are related as in documentation article you might have something like that:
var RestaurantIDRestaurantTbl = db.Restaurants.SingleOrDefault(x => x.RestaurantID == something);
if(RestaurantIDRestaurantTbl != null)
{
var listOfRestaurantsReservations = RestaurantIDRestaurantTbl.RestaurantReservationEvents.ToList();
}
{
// This will give you a list of IDs
var RestaurantIDRestaurantTbl = db.Restaurants
.Select(p => p.RestaurantID)
.ToList();
// Using .Any() is a better choice instead of .Contains()
// .Contains is used to check if a list contains an item while .Any will look for an item in a list with a specific ID
var listOfRestaurantsReservations = db.RestaurantReservationEvents
.Where(p => RestaurantIDRestaurantTbl.Any(r => r.pRestaurantID == p))
.ToList();
}
I have a query that looks like this:
var caseList = (from x in context.Cases
where allowedCaseIds.Contains(x => x.CaseId)
select new Case {
CaseId = x.CaseId,
NotifierId = x.NotifierId,
Notifier = x.NotifierId.HasValue ? new Notifier { Name = x.Notifier.Name } : null // This line throws exception
}).ToList();
A Case class can have 0..1 Notifier
The query above will result in the following System.NotSupportedException:
Unable to create a null constant value of type 'Models.Notifier'. Only entity types, enumeration types or primitive types are supported
in this context.
At the moment the only workaround I found is to loop the query result afterwards and manually populate Notifierlike this:
foreach (var c in caseList.Where(x => x.NotifierId.HasValue)
{
c.Notifier = (from x in context.Notifiers
where x.CaseId == c.CaseId
select new Notifier {
Name = x.Name
}).FirstOrDefault();
}
But I really don't want to do this because in my actual scenario it would generate hundreds of additional queries.
Is there any possible solution for a situation like this?.
I think you need to do that in two steps. First you can fetch only the data what you need with an anonymous type in a single query:
var caseList = (from x in context.Cases
where allowedCaseIds.Contains(x => x.CaseId)
select new {
CaseId = x.CaseId,
NotifierId = x.NotifierId,
NotifierName = x.Notifier.Name
}).ToList();
After that, you can work in memory:
List<Case> cases = new List<Case>();
foreach (var c in caseList)
{
var case = new Case();
case.CaseId = c.CaseId;
case.NotifierId = c.NotifierId;
case.NotifierName = c.NotifierId.HasValue ? c.NotifierName : null;
cases.Add(case);
}
You could try writing your query as a chain of function calls rather than a query expression, then put an .AsEnumerable() in between:
var caseList = context.Clases
.Where(x => allowedCaseIds.Contains(x.CaseId))
.AsEnumerable() // Switch context
.Select(x => new Case() {
CaseId = x.CaseId,
NotifierId = x.NotifierId,
Notifier = x.NotifierId.HasValue
? new Notifier() { Name = x.Notifier.Name }
: null
})
.ToList();
This will cause EF to generate an SQL query only up to the point where you put the .AsEnumerable(), further down the road, LINQ to Objects will do all the work. This has the advantage that you can use code that cannot be translated to SQL and should not require a lot of changes to your existing code base (unless you're using a lot of let expressions...)
I have a query that returns a list of currencies and joins to a lookup table. The result is then put into a class object (which works fine):
var queryforobject = from x in db.CurrencyExchangeRates.AsNoTracking()
join c in db.CurrencyTypes.AsNoTracking() on x.CurrencyTypeID equals c.ID
orderby x.ID
select new CurrencyExchangeRateObject
{
ID = x.ID,
CurrencyID = c.ID,
Currency = c.Description,
ExchangeRate = x.ExchangeRate,
LastEditedDate = x.LastEditedDate,
LastEditedBy = x.LastEditedBy,
Active = x.Active
};
I want to make this more dynamic, so if no CurrencyTypeID is supplied then it will return the full list (as it does already) - otherwise if a CurrencyTypeID is supplied it will only show where X.CurrencyTypeID = ID.
Something along the lines of an inline if?
There are a few options for filtering the query based on CurrencyTypeID if a search value (named currencyTypeID in this answer) is supplied, but return all data if no currencyTypeID is supplied.
First option: You could add a where clause to your existing query expression. The WHERE clause below will return every record in the data set if null is passed in for the currencyTypeID variable, otherwise it will filter the results.
from x in db.CurrencyExchangeRates.AsNoTracking()
join c in db.CurrencyTypes.AsNoTracking() on x.CurrencyTypeID equals c.ID
where (currencyTypeID == null || x.CurrencyTypeID == currencyTypeID)
orderby x.ID
select new CurrencyExchangeRateObject {
ID = x.ID,
CurrencyID = c.ID,
Currency = c.Description,
ExchangeRate = x.ExchangeRate,
LastEditedDate = x.LastEditedDate,
LastEditedBy = x.LastEditedBy,
Active = x.Active
};
Alternatively: Since queryforobject is of type IQueryable<T>, you can use LINQ's fluent API to append a WHERE clause to the query inside an if statement. You need to be more careful about timing on this one though as it needs to be done before you force evaluation of the IQueryable with a foreach loop, .ToList(), .Select() or other LINQ methods that force evaluation.
if(currencyTypeID != null)
queryforobject = queryforobject.Where(cerObj => cerObj.CurrencyID == currencyTypeID);
Here's my code:
var myStrings = (from x in db1.MyStrings.Where(x => homeStrings.Contains(x.Content))
join y in db2.MyStaticStringTranslations on x.Id equals y.id
select new MyStringModel()
{
Id = x.Id,
Original = x.Content,
Translation = y.translation
}).ToList();
And I get the error that the specified LINQ expression contains references to queries that are associated with different contexts. I know that the problem is that I try to access tables from both db1 and db2, but how do I fix this?
MyStrings is a small table
Load filtered MyStrings in memory, then join with MyStaticStringTranslations using LINQ:
// Read the small table into memory, and make a dictionary from it.
// The last step will use this dictionary for joining.
var byId = db1.MyStrings
.Where(x => homeStrings.Contains(x.Content))
.ToDictionary(s => s.Id);
// Extract the keys. We will need them to filter the big table
var ids = byId.Keys.ToList();
// Bring in only the relevant records
var myStrings = db2.MyStaticStringTranslations
.Where(y => ids.Contains(y.id))
.AsEnumerable() // Make sure the joining is done in memory
.Select(y => new {
Id = y.id
// Use y.id to look up the content from the dictionary
, Original = byId[y.id].Content
, Translation = y.translation
});
You are right that db1 and db2 can't be used in the same Linq expression. x and y have to be joined in this process and not by a Linq provider. Try this:
var x = db1.MyStrings.Where(xx => homeStrings.Contains(xx.Content)).ToEnumerable();
var y = db2.MyStaticStringTranslations.ToEnumerable();
var myStrings = (from a in x
join b in y on x.Id equals y.id
select new MyStringModel()
{
Id = x.Id,
Original = x.Content,
Translation = y.translation
}).ToList();
Refer to this answer for more details: The specified LINQ expression contains references to queries that are associated with different contexts
dasblinkenlight's answer has a better overall approach than this. In this answer I'm trying to minimize the diff against your original code.
I also faced the same problem:
"The specified LINQ expression contains references to queries that are associated with different contexts."
This is because it's not able to connect to two context at a time so i find the solution as below.
Here in this example I want to list the lottery cards with the owner name but the Table having the owner name is in another Database.So I made two context DB1Context and DB2Context.and write the code as follows:
var query = from lc in db1.LotteryCardMaster
from om in db2.OwnerMaster
where lc.IsActive == 1
select new
{
lc.CashCardID,
lc.CashCardNO,
om.PersonnelName,
lc.Status
};
AB.LottryList = new List<LotteryCardMaster>();
foreach (var result in query)
{
AB.LottryList.Add(new LotteryCardMaster()
{
CashCardID = result.CashCardID,
CashCardNO = result.CashCardNO,
PersonnelName =result.PersonnelName,
Status = result.Status
});
}
but this gives me the above error so i found the other way to perform joining on two tables from diffrent database.and that way is as below.
var query = from lc in db1.LotteryCardMaster
where lc.IsActive == 1
select new
{
lc.CashCardID,
lc.CashCardNO,
om.PersonnelName,
lc.Status
};
AB.LottryList = new List<LotteryCardMaster>();
foreach (var result in query)
{
AB.LottryList.Add(new LotteryCardMaster()
{
CashCardID = result.CashCardID,
CashCardNO = result.CashCardNO,
PersonnelName =db2.OwnerMaster.FirstOrDefault(x=>x.OwnerID== result.OwnerID).OwnerName,
Status = result.Status
});
}