That title is not very good, so consider the following. I have five tables:
User {
Id,
ProfileId // -> Profiles.Id
}
Profile {
Id
}
ProfilePermissionSets {
ProfileId // -> Profiles.Id
PermissionSetId // -> PermissionSets.Id
}
UserPermissionSets {
UserId // -> Users.Id
PermissionSetId // -> PermissionSets.Id
}
PermissionSets {
Id
}
Permissions {
Id,
PermissionSetId // -> PermissionSets.Id
}
And I want get all of the permissions for a user that are directly linked to it or indirectly through the profile. The not-quite-there SQL I've come up with so far is this:
SELECT [Pe].[Controller],
[Pe].[Action]
FROM [PermissionSets] AS [PS]
JOIN [UserPermissionSets] AS [UPS]
ON ([UPS].[PermissionSetId] = [PS].[Id])
JOIN [Users] AS [U]
ON ([U].[Id] = [UPS].[UserId])
JOIN [Profiles] AS [P]
ON ([P].[Id] = [U].[ProfileId])
JOIN [ProfilePermissionSets] AS [PPS]
ON ([PPS].[ProfileId] = [P].[Id])
JOIN [Permissions] AS [Pe]
ON ([Pe].[PermissionSetId] = [PS].[Id])
WHERE [U].[Id] = 4;
It returns back a correct count of rows, but it's repeating the controller or action over and over, so it's wrong. I'm hoping someone can help me correct it to show all of the distinct permission sets for the user. Ideally, I'd like to also change it so that it's all discovered starting at the user because that is what I have access to in the method I need to do this (the object is an Entity Framework class named User and will be browsed using LINQ).
UPDATED because I forgot that I really wanted the permissions not the permission sets.
Try this SQL
SELECT [Pe].[Controller],
[Pe].[Action]
FROM [Users] AS [U]
LEFT OUTER JOIN [UserPermissionSets] AS [UPS]
ON ([UPS].[UserId] = [U].[Id])
LEFT OUTER JOIN [ProfilePermissionSets] AS [PPS]
ON ([PPS].[ProfileId] = [U].[ProfileId])
LEFT OUTER JOIN [Permissions] AS [Pe]
ON ([Pe].[PermissionSetId] = [UPS].[PermissionSetId])
OR ([Pe].[PermissionSetId] = [PPS].[PermissionSetId])
WHERE [U].[Id] = 4;
So, messing around on LINQPad, I came up with this as far as the LINQ query:
user.PermissionSets.Union(user.Profile.PermissionSets).SelectMany(
ps =>
ps.Permissions.Select(
p =>
p.Controller + "." + p.Action));
And it produces what I want, BUT it does it by composing the results of a bunch of SQL queries. The biggest impact comes from profiles that have multiple permission sets, like say the Administrator. I don't think there's a way around it, and I only have a User object to work with, so I'm ok with the excess SQL queries, at least for now.
Related
I've got this horrible EF code in a code base and I am scratching my head trying to figure out what it's trying to do. If I were looking to put this in a stored procedure instead how would this query look in SQL?
public void LoadNotifyListItems(UserProfileModel user, DbContext pc)
{
var allowedEvents = (from r in user.Roles
join near in pc.NotifyEventAllowedRoles on r.RoleId equals near.RoleId
join ne in pc.NotifyEvents on near.NotifyEventId equals ne.Id
select ne).Distinct();
var NotifyListItems = from ne in allowedEvents
join pune in pc.UserNotifyEvents
on new { NotifyEventId = ne.Id, UserId = user.Id }
equals new { pune.NotifyEventId, pune.UserId }
into loj
from pune in loj.DefaultIfEmpty()
select new NotifyListItem
{
Event = ne,
Value = pune ?? new UserNotifyEvent
{
NotifyEventId = ne.Id
}
};
}
The issue I am having is the entirety of pc.UserNotifyEvents table is being queried. Monitoring the DB, EF is making this query when the LoadNotifyListItems method is being run:
[Extent1].[UserId] AS [UserId],
[Extent1].[NotifyEventId] AS [NotifyEventId],
[Extent1].[NotifyPrimaryEmail] AS [NotifyPrimaryEmail],
[Extent1].[NotifyAlternateEmail] AS [NotifyAlternateEmail],
[Extent1].[NotifySmsNumber] AS [NotifySmsNumber],
[Extent1].[Threshold1] AS [Threshold1],
[Extent1].[Threshold2] AS [Threshold2]
FROM [UserNotifyEvents] AS [Extent1]
This isn't needed and its ingesting 200,000 rows everytime. I am thinking of moving the query to a Stored Proc and pass in userId as a parameter instead
Please let me know if there is not enough information to go off here.
The first part of the query is joining a list of roles in memory (user.Roles) with an IQueryable from the DbContext. This is probably the cause of your issue. You should use the pc.Roles property instead, and introduce a where clause to filter by the id of the provided user.
If user info is in same database as other tables
Pass the user id to the stored proc. Inside the sproc, join user-roles to the other tables WHERE userid = passed-in parameter.
If user info is NOT in same database as other tables
First, get a list of role ids for the user.
var roleIds = user.Roles.Select(i=>i.RoleId).Distinct().ToList();
Then you're going to pass that list of role IDs to your stored proc. Try something like this Stack Overflow article, which tells you how to pass those role IDs as distinct strings to a stored proc.
The rest of this appears to be regular table joins within the database. The most critical part is getting those roles IDs for the user.
I have 2 tables:
Users data with PK UserId
UsersOrders with FK UserID
I want to do LINQ query to give me a list of users with member that is the list of the user orders.
I've tried:
var myNestedData = (from ub in db.Users
join ah in db.UsersOrders on ub.UserId equals ah.UserId
into joined
from j in joined.DefaultIfEmpty()
group j by new { ub.UserId, ub.UserName, ub.UserPhone, ub.Approved } into grouped
where grouped.Key.Approved == true
select new
{
UserId= grouped.Key.UserId,
UserName = grouped.Key.UserName,
UserPhone = grouped.Key.UserPhone,
Orders = grouped
}).ToList();
The problem is that I'm getting inside Orders an Object<a,UsersOrders>, which I don't expect.
Is this the right way to approach a solution in terms of performance?
It seems to me you should be able to keep this simple:
var myNestedData = (from u in db.Users
where u.Approved == true
select new
{
User = u,
Orders = u.UserOrders
}).ToList();
You are trying to get a list of all the Approved users and that users orders. Am I missing something?
ALso the name of u.UserOrders will depend on how you have configured your mapping.
Change it to
Orders = grouped.ToList()
To see if it's a potential performance issue, look at the SQL that is generated to see if it is an N+1 scenario (meaning it runs one query for the parent record and N queries for the child records.
I say potential because while may not be the fasted method, it may not be an issue. If there is no noticeable performance problems I wouldn't worry about it and instead focus on making the app better by improving the experience, which may include improving the perfornamce of other areas of the map.
Right problem I've been trying to sort for ages now!
I'm trying to display the result from a SQL select command and display this information to my view. I can't simple use EntityFramework, not from what I can see anyway, to do the command because I am bringing in data from 2 different tables and displaying as one, this is for a report.
this is the sql command I need to run.
select FirstName, LastName,
(select count(*) from Orders o where U.userID = o.CreatedByUserID and ProductID = 1) as ProductCount
from Users U
order by UserID
Is there anything I can do to run this command? I'd be willing to try a EntityFramwork way of doing it if I can't run the SQL directly.
Thanks in advance for any help!
You could use EF (or another ORM solution). There is clearly a relationship between the orders and the users: U.userID = o.CreatedByUserID. You can just retrieve the users, and then access user.Orders.Count to get the "missing" value.
Assuming ctx is your Context try
var q=from u in ctx.users
join x in ctx.orders.Where(y=>y.ProductID==1).GroupBy(y => y.CreatedByUserID)
on u.UserID equals x.Key into joined_orders
from o in joined_orders.DefaultIfEmpty()
select new {
FirstName=u.FirstName,
LastName=u.LastName,
Count=o.Count()
};
This assumes that you don't have any Relationships defined in your model, otherwise you could just use
var q=from u in ctx.users
select new {
FirstName=u.FirstName,
LastName=u.LastName,
Count=o.Count(x=>x.ProductID==1)
};
I have the following LINQ query, that is returning the results that I expect, but it does not "feel" right.
Basically it is a left join. I need ALL records from the UserProfile table.
Then the LastWinnerDate is a single record from the winner table (possible multiple records) indicating the DateTime the last record was entered in that table for the user.
WinnerCount is the number of records for the user in the winner table (possible multiple records).
Video1 is basically a bool indicating there is, or is not a record for the user in the winner table matching on a third table Objective (should be 1 or 0 rows).
Quiz1 is same as Video 1 matching another record from Objective Table (should be 1 or 0 rows).
Video and Quiz is repeated 12 times because it is for a report to be displayed to a user listing all user records and indicate if they have met the objectives.
var objectiveIds = new List<int>();
objectiveIds.AddRange(GetObjectiveIds(objectiveName, false));
var q =
from up in MetaData.UserProfile
select new RankingDTO
{
UserId = up.UserID,
FirstName = up.FirstName,
LastName = up.LastName,
LastWinnerDate = (
from winner in MetaData.Winner
where objectiveIds.Contains(winner.ObjectiveID)
where winner.Active
where winner.UserID == up.UserID
orderby winner.CreatedOn descending
select winner.CreatedOn).First(),
WinnerCount = (
from winner in MetaData.Winner
where objectiveIds.Contains(winner.ObjectiveID)
where winner.Active
where winner.UserID == up.UserID
orderby winner.CreatedOn descending
select winner).Count(),
Video1 = (
from winner in MetaData.Winner
join o in MetaData.Objective on winner.ObjectiveID equals o.ObjectiveID
where o.ObjectiveNm == Constants.Promotions.SecVideo1
where winner.Active
where winner.UserID == up.UserID
select winner).Count(),
Quiz1 = (
from winner2 in MetaData.Winner
join o2 in MetaData.Objective on winner2.ObjectiveID equals o2.ObjectiveID
where o2.ObjectiveNm == Constants.Promotions.SecQuiz1
where winner2.Active
where winner2.UserID == up.UserID
select winner2).Count(),
};
You're repeating join winners table part several times. In order to avoid it you can break it into several consequent Selects. So instead of having one huge select, you can make two selects with lesser code. In your example I would first of all select winner2 variable before selecting other result properties:
var q1 =
from up in MetaData.UserProfile
select new {up,
winners = from winner in MetaData.Winner
where winner.Active
where winner.UserID == up.UserID
select winner};
var q = from upWinnerPair in q1
select new RankingDTO
{
UserId = upWinnerPair.up.UserID,
FirstName = upWinnerPair.up.FirstName,
LastName = upWinnerPair.up.LastName,
LastWinnerDate = /* Here you will have more simple and less repeatable code
using winners collection from "upWinnerPair.winners"*/
The query itself is pretty simple: just a main outer query and a series of subselects to retrieve actual column data. While it's not the most efficient means of querying the data you're after (joins and using windowing functions will likely get you better performance), it's the only real way to represent that query using either the query or expression syntax (windowing functions in SQL have no mapping in LINQ or the LINQ-supporting extension methods).
Note that you aren't doing any actual outer joins (left or right) in your code; you're creating subqueries to retrieve the column data. It might be worth looking at the actual SQL being generated by your query. You don't specify which ORM you're using (which would determine how to examine it client-side) or which database you're using (which would determine how to examine it server-side).
If you're using the ADO.NET Entity Framework, you can cast your query to an ObjectQuery and call ToTraceString().
If you're using SQL Server, you can use SQL Server Profiler (assuming you have access to it) to view the SQL being executed, or you can run a trace manually to do the same thing.
To perform an outer join in LINQ query syntax, do this:
Assuming we have two sources alpha and beta, each having a common Id property, you can select from alpha and perform a left join on beta in this way:
from a in alpha
join btemp in beta on a.Id equals btemp.Id into bleft
from b in bleft.DefaultIfEmpty()
select new { IdA = a.Id, IdB = b.Id }
Admittedly, the syntax is a little oblique. Nonetheless, it works and will be translated into something like this in SQL:
select
a.Id as IdA,
b.Id as Idb
from alpha a
left join beta b on a.Id = b.Id
It looks fine to me, though I could see why the multiple sub-queries could trigger inefficiency worries in the eyes of a coder.
Take a look at what SQL is produced though (I'm guessing you're running this against a database source from your saying "table" above), before you start worrying about that. The query providers can be pretty good at producing nice efficient SQL that in turn produces a good underlying database query, and if that's happening, then happy days (it will also give you another view on being sure of the correctness).
First I'll explain my scenario. Here's the 2 general tables in use:
Accounts:
LegacyID = The identifer of the
account
InvoiceAccount = An account can have
multiple subaccounts in the same
table. This field is the LegacyID
of its parent account
Customer = There can be two different
types of accounts, and they can share
ID's - so this column differentiates
between the 2. For the purposes of my
SQL I always want it to be true rather
than false
AllocatedUser = The username of the
person who needs to see this
deliveries. This is only populated on
the parent account, so I need to link
back to get this for the subaccounts
Deliveries:
LegacyID = The deliveries identifier
Customer = The LegacyID of the account
related to the delivery (can be a
subaccount)
OnHold = A flag which for the purposes
of my query needs to be 'true'
Now that's explained, basically I need an SQL which returns any deliveries that are 'OnHold', but only for deliveries for accounts that are allocated the logged in user. The query for selecting the AllocatedUser if the delivery links to a parent account was simple, but I'm having issues with returning rows if the delivery is linked to a subaccount - it's simply not returning any. Here is the SQL below:
SELECT Deliveries_1.LegacyID, Deliveries_1.TripDate, Deliveries_1.OnHoldReason, Account_2.AllocatedUser
FROM Deliveries AS Deliveries_1 INNER JOIN
Account AS Account_1 ON Deliveries_1.Customer = Account_1.InvoiceAccount INNER JOIN
Account AS Account_2 ON Account_1.InvoiceAccount = Account_2.LegacyID
WHERE (Deliveries_1.OnHold = #OnHold) AND (Account_2.Customer = 'True') AND (Account_2.AllocatedUser = #AllocatedUser)
My mind is frazzled from trying to work out why it don't work at the moment - I'd really appreciate any advice.
Thanks!
It sounds like you're looking for records in the Deliveries table that are associated with a user's account, the user's direct parent's account, and the user's direct child's account. If this is true modifying your query as follows should do the trick.
SELECT DISTINCT d.LegacyID, d.TripDate, d.OnHoldReason,
case
when child.LegacyID = d.Customer then child.AllocatedUser
when parent.LegacyID = d.Customer then parent.AllocatedUser
else this.AllocatedUser
end as 'Delivery_AllocatedUser'
FROM Account this
LEFT JOIN Account parent on parent.LegacyID = this.InvoiceAccount
LEFT JOIN Account child on this.LegacyID = child.InvoiceAccount
JOIN Deliveries d on (d.Customer = this.LegacyID AND this.Customer = 'True')
OR (d.Customer = parent.LegacyID AND parent.Customer = 'True')
OR (d.Customer = child.LegacyID AND child.Customer = 'True')
WHERE d.OnHold = #OnHold
AND this.AllocatedUser = #AllocatedUser
First comment is that you should build the joins in steps if you are having issues.
Second comment is that in the query you posted there is unnecessary join - you are not filtering nor selecting nor joining on anything specific from table aliased as Account_1.
Maybe that is an indication of what you are doing wrong. Otherwise, the query looks(!) ok (assuming that all the joins are correct; your descriptions are not exact - for example for Accounts.Customer you say that 'For the purposes of my SQL I always want it to be true rather than false' - well the Customer is not true/false field is it? What do you mean? There are other such inconsistencies...)
Some sample data and a sample of the result could shed some more light.
ON Deliveries_1.Customer = Account_1.InvoiceAccount
ON Account_1.InvoiceAccount = Account_2.LegacyID
If both of these criteria are true, then Delivery.Customer would equal Account2.LegacyId (delivery would have to be on the parent account). I don't think that's what you intended.
Perhaps you meant
ON Deliveries_1.Customer = Account_1.LegacyID
ON Account_1.InvoiceAccount = Account_2.LegacyID