Equivalent Sql group by to Linq to get value and text properties - c#

I've been spending quite a long time trying to make a Linq query with a group by in it, just to return a List for a SelectList with the common Text an Value properties.
I think and still have hope that it does not have to be as difficult as it looks, and i prefer to think that is me and my tired brain sometimes, but honestly i gave up.
I've been able to achieve what i wanted with raw sql but, it's just simple curiosity because I deny to think Linq is that much of a time waster...
So please if someone out there, who knows how to do this in a Linq lambda query, please illuminate my tired brain...
What i got with raw sql and want with linq lambda (it works perfectly, took me 2 minutes and I can populate my MultiSelectFor nicely):
string sql = " select [AlphabeticCode] as 'Value', concat([AlphabeticCode], ' - ', Currency) as Text from [table] " +
" group by AlphabeticCode, Currency; ";
var currency = db.Database
.SqlQuery<Models.myViewModels.LinqToSql.SelectListViewModel>(sql)
.ToList();
model.currency = new SelectList(currency, "Value", "Text");
What i thought i was doing right, made me spend a lonnnng time and ended up giving me a collection inside my text property... not what i wanted at all... (I know is not correct, i tried quite a few more options), but this one kinda made sense to me, please, give a reason to keep working with this weird Linq thing...
var cc = db.table.GroupBy(item => item.Currency)
.Select(group =>
new
{
Value = group.Key,
Text = group.Select(m => m.Currency).Take(1), // here is where i want just ONE string that is the currency name
}
)
.ToList();
Thank you so much to whoever takes some time to read my frustrations...

I am not sure if this is what you need but I think this will make the same query as your SQL
var items = from item in table
group item by new { item.AlphabeticCode, item.Currency } into g
select new { Value = g.Key.AlphabeticCode, Text = g.Key.AlphabeticCode + " - " + g.Key.Currency };
//Fluent or inline
table.
GroupBy(i => new { i.AlphabeticCode, i.Currency }).
Select(g => new { Value = g.Key.AlphabeticCode, Text = g.Key.AlphabeticCode + " - " + g.Key.Currency });

Related

GroupBy with dyanmic multiple fields in Linq

I have a Datatable result. I want to group by with some of the fields may be one or two or three. The main fact is developers can't know the numbers of fields clients want to group. So, for that case, I find some solutions about putting List parameters to group by fields. But can't find the proper one yet. Is there anybody who could help me with it? I was stuck with this issue for three days.
Use reflection to dynamically group your DB-rows:
//something like:
var props = typeof(Foo).GetProperties().Where( x=> x.Name.Equals("MyField");
var result = rows.GroupBy( x => prop.GetValue( x ) );
For multiple fields, loop over them and apply the GroupBy to the result of the last iteration.
I got it by using the groupby in Dynamic LinQ.
Like this
var qfields = string.Join(",", stringarray.Select(x => "it[\"" + x + "\"] as " + x));
var collVal = datatableresult.AsQueryable()
.GroupBy("new (" + qfields + ")", "it")
.Select("new (it.Key as Key,it as Data)");
This code group by the data table successfully but I still need to do the order by with multiple columns.

LINQ subquery returns brackets and fieldnames, how to get just the value?

My LINQ query is the following, I query two tables, Settlements and Bills which have a one to many relationship, in particular there can be one settlement with one or more bills.
var TheSettlements = from settlement in context_.Settlements
select new
{
asettlementid = settlement.SettlementId,
SqBills = string.Join(",",
(
from b in context_.Bills
.Where(b => b.SettlementId == settlement.SettlementId)
select new { b.BillMunicipalityId }
))
};
Now the trivial part (for me), is that I would like the bills concatenated, so after many hours of trial and error, I got my results but the Bills(BillMunicipalityId) are presented inside brackets including the fieldname, like this.
The way I export the data, to a txt to be more precise, is this.
foreach (var settlement in TheSettlements)
{
SettlementsText
.Append(settlement.asettlementid).Append(Delimiter)
.Append(settlement.SqBills.ToString()).Append(Delimiter)
.Append(Newline);
}
And the results I get in the txt.
3,{ BillMunicipalityId = f9e47f81-fc97-4008-b93d-d384230c53aa },
6,,
7,{ BillMunicipalityId = 8b66610a-20c1-4f47-9f37-489d1a8ce31a },{ BillMunicipalityId = 003d59d4-7bcb-4603-b42c-dc389dd8fb06 },{ BillMunicipalityId = 0070bb29-e3a1-4317-b5e2-3d1ef08dd20b },
How should I handle this to get only the values?
Just the GUID of every BillMunicipalityId, without the { BillMunicipalityId = } part.
I think rather than selecting as a new object you could just select the value like this:
var TheSettlements = from settlement in context_.Settlements
select new
{
asettlementid = settlement.SettlementId,
SqBills = string.Join(",",
(
from b in context_.Bills
.Where(b => b.SettlementId == settlement.SettlementId)
select b.BillMunicipalityId
))
};
Your Linq statement looks really strange to me. As it shows in the question you're mixing Linq with extensions methods.
If context is a DbContext which is going to the database, concating the results with string.Join won't work as this statement can't be translated to SQL code. If context however contains in memory data this may work. I advise however to not use string.Join within Linq unless you add a clear comment to the code, this Linq should never hit the database.
When this code will hit the database, you'll get an NotSupportedException with the message:
LINQ to Entities does not recognize the method 'System.String Join[Int32]'
The second thing I notice in your query, normally the one-to-many relation is known by the datamodel and you shouldn't need to join the results yourself.
The easiest way to solve this, is to use an intermediate query, which gets the results from the database and after running the query and getting the data into memory, perform the conversion with string.Join()
This would look like:
var TheSettlements =
from settlement in context_.Settlements
select new
{
asettlementid = settlement.SettlementId,
SqBills = (
from b in settlement.Bills
select b.BillMunicipalityId
).ToList(),
};
// Get the results in memory:
var results = TheSettlements.ToArray();
// Format the results:
var printResults = results.Select(s =>
s.asettlementid.ToString() + ", " + string.Join(", ",s.SqBills));

LINQ - Comparing a List of strings against a colon separated field

Is it possible to do a LINQ where clause and split a varchar field on a : into a collection and compare this to another collection to see if any values match.
For example, we have a List<string> called AllowedHolders which contains ARR ACC etc.. however the field in the database (which unfortunately we cannot change) is a varchar but has values separated by a colon: i.e. ":ARR:ACC:"
What we would like to do is write a LINQ where clause which can check if any of the AllowedHolders appear in the field. As we wish to restrict the results to only bring back records where the field contains a value in the AllowedHolders collection.
We have done the following where the field only contains a single value
searchResults = searchResults.Where(S => searchParams.AllowedBusinessAreas.Contains(S.SIT_BusinessArea));
But the following will not work because SIT_HolderNames contains values separated by a colon:
searchResults = searchResults.Where(S => searchParams.AllowedHolders.Contains(S.SIT_HolderName)
Any ideas would be much appreciated. If you need me to explain anything further please let me know.
Andy
Use Intersect(), Any() and String.Split().
searchResults = searchResults.Where(s => searchParams.AllowedHolders.Intersect(S.SIT_HolderName.Split(':')).Any());
For example:
":ACC:TEST::ARR:".Split(':') -> string[] { "", "ACC", "TEST", "", "ARR", "" };
You can notice the empty strings, if you don't want to take them into account use String.Split(char[], StringSplitOptions.RemoveEmptyEntries):
":ACC:TEST::ARR:".Split(new char[] {':'}, StringSplitOptions.RemoveEmptyEntries) -> string[] { "ACC", "TEST", "ARR" };
UPDATE
You will have to fetch the data before calling String.Split() using ToList().
searchResults = searchResults.ToList().Where(s => searchParams.AllowedHolders.Intersect(S.SIT_HolderName.Split(':')).Any());
If the data in searchResults is too big what you can do is to fetch only a primary key and the SIT_HolderName.
var keys = searchResults.Select(s => new { Key = s.SIT_PKey, SIT_HolderName = s.SIT_HolderName })
.ToList()
.Where(s => searchParams.AllowedHolders.Intersect(s.SIT_HolderName.Split(':')).Any())
.Select(s => s.Key)
.ToList();
searchResult = searchResults.Where(s => keys.Contains(s.SIT_PKey));
I don't know what can be the performances of the above query. Otherwise, you can try with a Join():
searchResult = searchResults.Join(keys, s => s.SIT_PKey, key => key, (s, key) => s);
Maybe you can use:
searchResults = searchResults.Where(S => searchParams.AllowedHolders
.Any(H => S.SIT_HolderName.Contains(H))
);
or
searchResults = searchResults.Where(S => searchParams.AllowedHolders
.Any(S.SIT_HolderName.Contains)
);
As pointed out by the first comment, this only works if no holder name contains another holder name as a substring. I was implicitly assuming that all your holder names were three-letter strings like ARR and ACC. If this is not the case, consider using (":" + H + ":"), or find a more safe solution.
Edit: Just for completeness, here are two versions with colons prepended and appended:
// OK if some name is contained in another name as a substring
// Requires colon before the first and after the last name
searchResults = searchResults.Where(S => searchParams.AllowedHolders
.Any(H => S.SIT_HolderName.Contains(":" + H + ":"))
);
And:
// OK if some name is contained in another name as a substring
// Ugly checks included to handle cases where no colons are present in the extreme ends
searchResults = searchResults.Where(S => searchParams.AllowedHolders
.Any(H => S.SIT_HolderName.Contains(":" + H + ":") || S.SIT_HolderName.StartsWith(H + ":") || S.SIT_HolderName.EndsWith(":" + H) || S.SIT_HolderName == H)
);
If in the DB column's values the separators are indeed in the format:
:AAA:BBB:CCC:DDD:
and not just (please note the first and last character!)
AAA:BBB:CCC:DDD
then you may perform a LIKE lookup:
select .... from ... where column LIKE '%:BBB:%'
which translates into LINQ:
var idWithColons = ":" + "BBB" + ":";
from ... where .... column.Contains(idWithColons)
for many possible IDS, you'd have to produce:
select .... from ... where column LIKE '%:BBB:%' OR column LIKE '%:DDD:%' OR ..
which translates into LINQ:
var idWithColons = ":" + "BBB" + ":";
var idWithColons2 = ":" + "DDD" + ":";
from ... where .... column.Contains(idWithColons) or column.Contains(idWithColons2)
But that's good only for small number of alternatives. For unknown list of IDs, you can try to rewrite it as a dynamically built filter, but that's not so easy if you are not familiar with Expression<Func<>>.. Anyways, searching via LIKE is not so good idea anyways.. but that's not many other options :/
otherwise, well, that's unpretty.. you could probably prepare a scalar-valued function on the sql server and register it somehow in your linq provider, but I dont think it's worth it..
EDIT:
building where-clause dynamically is explained ie. here http://www.albahari.com/nutshell/predicatebuilder.aspx - look for the PredicateBuilder. The builder is actually generic and will be directly usable, but you still will have to write the small loop that concatenates OR-LIKE by yourself. I think the article is well written enough, drop me anote if you find any problems. except for the performance. LIKE %% is not fast.

Numbering the rows per group after an orderby in LINQ

Say you have columns AppleType, CreationDate and want to order each group of AppleType by CreationDate. Furthermore, you want to create a new column which explicitly ranks the order of the CreationDate per AppleType.
So, the resulting DataSet would have three columns, AppleType, CreationDate, OrderIntroduced.
Is there a LINQ way of doing this? Would I have to actually go through the data programmatically (but not via LINQ), create an array, convert that to a column and add to the DataSet? I have there is a LINQ way of doing this. Please use LINQ non-method syntax if possible.
So are the values actually appearing in the right order? If so, it's easy - but you do need to use method syntax, as the query expression syntax doesn't support the relevant overload:
var queryWithIndex = queryWithoutIndex.Select((x, index) => new
{
x.AppleType,
x.CreationDate,
OrderIntroduced = index + 1,
});
(That's assuming you want OrderIntroduced starting at 1.)
I don't know offhand how you'd then put that back into a DataSet - but do you really need it in a DataSet as opposed to in the strongly-typed sequence?
EDIT: Okay, the requirements are still unclear, but I think you want something like:
var query = dataSource.GroupBy(x => x.AppleType)
.SelectMany(g => g.OrderBy(x => x.CreationDate)
.Select((x, index ) => new {
x.AppleType,
x.CreationDate,
OrderIntroduced = index + 1 }));
Note: The GroupBy and SelectMany calls here can be put in query expression syntax, but I believe it would make it more messy in this case. It's worth being comfortable with both forms.
If you want a pure Linq to Entities/SQL solution you can do something like this:
Modified to handle duplicate CreationDate's
var query = from a in context.AppleGroup
orderby a.CreationDate
select new
{
AppleType = a.AppleType,
CreationDate = a.CreationDate,
OrderIntroduced = (from b in context.AppleGroup
where b.CreationDate < a.CreationDate
select b).Count() + 1
};

LINQ to SQL Peculiarities

I'm encountering some peculiarities with LINQ to SQL.
With a relatively simple query, I want to select some fields, but have the date fields formatted as strings, which I first achieved like this:
var list = dataContext.MyLists.Single(x => x.ID == myId);
var items = from i in list.MyItems
select
new
{
i.ID,
i.Sector,
i.Description,
CompleteDate = i.CompleteDate.HasValue ? i.CompleteDate.Value.ToShortDateString() : "",
DueDate = i.DueDate.HasValue ? i.DueDate.Value.ToShortDateString() : ""
};
Later on I tried the following query, which is exactly the same, except I'm querying straight from my dataContext, rather than an element in my first query:
var items = from i in dataContext.MyLists
select
new
{
i.ID,
i.Sector,
i.Description,
CompleteDate = i.CompleteDate.HasValue ? i.CompleteDate.Value.ToShortDateString() : "",
DueDate = i.DueDate.HasValue ? i.DueDate.Value.ToShortDateString() : ""
};
The first one runs fine, yet the second query yields a:
Could not translate expression '...' into SQL and could not treat it as a local expression.
If I remove the lines that Format the date, it works fine. If I remove the .HasValue check it also works fine, until there are null values.
Any ideas?
Anthony
I'd do the SQL part without doing the formatting, then do the formatting on the client side:
var items = list.MyItems.Select(item => new { item.ID, item.Sector, item.Description,
item.CompleteDate, item.DueDate })
.AsEnumerable() // Don't do the next bit in the DB
.Select(item => new { item.ID, item.Sector, item.Description,
CompleteDate = FormatDate(CompleteDate),
DueDate = FormatDate(DueDate) });
static string FormatDate(DateTime? date)
{
return date.HasValue ? date.Value.ToShortDateString() : ""
}
In the first query, you have already got the data back from the database by the time the second line runs (var items = ...). This means that the 2nd line runs at the client, where ToShortDateString can run quite happily.
In the second query, because the select runs directly on an IQueryable collection (dataContext.MyLists), it attempts to translate the select into SQL for processing at the server, where ToShortDateString is not understood - hence the "Could Not Translate.." exception.
To understand this a bit better, you really need to understand the difference between IQueryable and IEnumerable, and at which point a Linq To Sql query stops being IQueryable and becomes IEnumerable. There is plenty of stuff on the web about this.
Hope this helps,
Paul
Just like the error message tells you, the difference is due to what can be done locally verses remotely while connecting to SQL.
The Linq code has to be converted by Linq to SQL into a SQL command for the remote data pulls - anything that has to be done locally cannot be included.
Once you pulled it into a local object (in the first example), it is not using Linq to SQL anymore, just plain Linq. At that point you are free to do local manipulations on it.
Maybe there was a copy and paste error or just a typo in your sample. But if not, this might be the problem...
In the second query you are querying a collection of lists, whereas in the first query you were querying the items within a list. But you haven't adjusted the query to account for this difference.
What you need might be this. Note the commented lines which did not appear in your second sample.
var items = from aList in dataContext.MyLists
from i in aList.MyItems // Access the items in a list
where aList.ID == myId // Use only the single desired list
select
new
{
i.ID,
i.Sector,
i.Description,
CompleteDate = i.CompleteDate.HasValue ? i.CompleteDate.Value.ToShortDateString() : "",
DueDate = i.DueDate.HasValue ? i.DueDate.Value.ToShortDateString() : ""
};
ToShortDateString() is not supported by Linq to SQL http://msdn.microsoft.com/en-us/library/bb882657.aspx

Categories