Dapper Multi Mapping Not Splitting on Named Parameters - c#

New to Dapper here! Having an issue with multi-mapping. This is my query:
var sql = #"select distinct a.*,
c.Id as 'GenreId', c.Active as 'GenreActive', c.Link as 'GenreLink', c.Name as 'GenreName', c.DateCreated as 'GenreDateCreated', c.DateEdited as 'GenreDateEdited',
d.Id as 'CommentId', d.ReviewId as 'CommentReviewId', d.Name as 'CommentName', d.Email as 'Comment.Email', d.Content as 'CommentContent', d.Active as 'CommentActive', d.DateCreated as 'CommentDateCreated', d.DateEdited as 'CommentDateEdited', d.CommentId as 'ReplyCommentId'
from Review a " +
"left join ReviewGenre b on a.Id = b.ReviewId " +
"left join Genre c on c.Id = b.ReviewId " +
"left join Comment d on a.Id = d.ReviewId " +
"where a.Active = 1 " +
"order by a.DatePublished desc;"
;
And my entities are (shortened for brevity):
public class Review
{
public int Id {get;set;}
public IEnumerable<Genre> Genres { get; set; }
public IEnumerable<Comment> Comments { get; set; }
}
public class Genre
{
public int Id {get;set;}
public string Name {get;set;}
}
public class Comment
{
public int Id {get;set;}
public int Content {get;set;
}
My query using Dapper tries to split on the renamed columns for Genre.Id and Comment.Id. The query appears to be working fine except none of the Genres and Comments appear to be mapping to the Review class. This is how I am trying to execute the query:
using (var connection = new SqlConnection(_ConnectionString))
{
var reviews = await connection.QueryAsync<Review, Genre, Comment, Review>(
sql,
(review, genre, comment) =>
{
review.Genres = new List<Genre>();
review.Comments = new List<Comment>();
if (genre != null)
{
review.Genres.ToList().Add(genre);
}
if (comment != null)
{
review.Comments.ToList().Add(comment);
}
return review;
},
commandType: CommandType.Text,
splitOn: "GenreId,CommentId");
return reviews;
}
I have researched throughout tutorials and SO on the subject and not finding what could be causing the mapping to not happen.
I would appreciate any suggestions (newbie to Dapper). Thanks!

At this line
review.Genres.ToList().Add(genre);
you are creating each time a new list (.ToList()). This method returns/creates new instance, but the new instance is never assigned back to the model property. It's like doing something like that:
var list = new List<int>();
new List<int>().Add(1);
The two instances are separate objects.
What you can do is to changed your models to work like this (the lists are instantiated with the creation of the object):
public class Review
{
public int Id {get;set;}
public List<Genre> Genres { get; set; } = new List<Genre>();
public List<Comment> Comments { get; set; } = new List<Comment>();
}
and then adding elements like this:
review.Genres.Add(genre);
Or you can check the original dapper tutorial where they are using dictionary as state manager to remove duplicates.

Related

Combining Objects into a new model with Linq/Lambda expressions

I am working on a Blazor Project and using Dapper to Pull Data from a SQL Db.
I am pulling 3 tables at the moment. An entity table, a specialty table and a bridge table that is there to maintain the many to many relationship between entity and specialties.
I am able to pull from SQL fine and I want to combine the data in my data service and inject it as a new object model to the Blazor component.
Here are the models:
Entity
public class EntityModel : IEntityModel
{
public int Id { get; set; }
public int PhysicianId { get; set; }
public int PartnerId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Specialty
public class SpecialtyModel : ISpecialtyModel
{
public int Id { get; set; }
public string Name { get; set; }
}
BridgeModel
public class BridgeModel : IBridgeModel
{
public int Id1 { get; set; }
public int Id2 { get; set; }
}
I made the properties in the bridge model generic so I could use it with another bridge table I have for a many to many relationship. The bridge tables are just two columns of IDs that link their respective tables. In this case Entity.Id and Specialty.Id
Here is the model I am combining all the information into:
public class CombinedModel : ICombinedModel
{
public int Id { get; set; }
public int PhysicianId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<ISpecialtyModel> Specialties { get; set; }
}
Here is the inside of my data service where I am stuck trying to combine the data with Linq and Lambda expressions.
public async Task<List<IEntityModel>> ReadEntities()
{
var entities = await _dataAccess.LoadData<EntityModel, dynamic>("dbo.spEntity_Read", new { }, "SqlDb");
return entities.ToList<IEntityModel>();
}
public async Task<List<ISpecialtyModel>> ReadSpecialties()
{
var specialties = await _dataAccess.LoadData<SpecialtyModel, dynamic>("dbo.spSpecialty_Read", new { }, "SqlDb");
return specialties.ToList<ISpecialtyModel>();
}
public async Task<List<IBridgeModel>> ReadEntitySpecialtyBridge()
{
var bridge = await _dataAccess.LoadData<BridgeModel, dynamic>("dbo.spEntitySpecialty_Read", new { }, "SqlDb");
return bridge.ToList<IBridgeModel>();
}
public async Task<List<ICombinedModel>> CombineData()
{
var entities = await ReadEntities();
var specialties = await ReadSpecialties();
var bridge = await ReadEntitySpecialtyBridge();
//var combined = (from e in entities
// join b in bridge on e.Id equals b.Id1
// join s in specialties on b.Id2 equals s.Id
// select new CombinedModel()
// {
// Id = e.Id,
// PhysicianId = e.PhysicianId,
// FirstName = e.FirstName,
// LastName = e.LastName,
// Specialties = new List<ISpecialtyModel>()
// });
var combined = (from e in entities
select new CombinedModel
{
Id = e.Id,
PhysicianId = e.PhysicianId,
FirstName = e.FirstName,
LastName = e.LastName,
Specialties = specialties.Where(s => )
}
);
return combined.ToList<ICombinedModel>();
This is where I am stuck. How can I write this Linq query to combine this data into the new model?
I am able to get data passed into the razor component but I am not combining it correctly and this is where I am stuck.
I hope someone can shed some light on the matter. Thank you for taking the time to look over this, I appreciate it.
With Thanks,
Cesar
If you wanted to process locally (doing on the server should eliminate the need to pull bridge over from the database, and if bridge contains records that aren't relevant to entities could potentially be a lot of unnecessary data traffic) then you just need to filter specialties by the correct bridge records for a given entity:
var combined = (from e in entities
select new CombinedModel {
Id = e.Id,
PhysicianId = e.PhysicianId,
FirstName = e.FirstName,
LastName = e.LastName,
Specialties = specialties.Where(s => bridge.Where(b => b.Id1 == e.Id).Select(b => b.Id2).Contains(s.Id)).ToList()
});
Depending on the size of specialties and entities, it might be worthwhile to pre-process bridge to make access for a given entity more efficient (Where is O(n) so specialties.Where x bridge.Where is O(n*m)):
var bridgeDict = bridge.GroupBy(b => b.Id1).ToDictionary(bg => bg.Key, bg => bg.Select(b => b.Id2).ToHashSet());
var combined = (from e in entities
let eBridge = bridgeDict[e.Id]
select new CombinedModel {
Id = e.Id,
PhysicianId = e.PhysicianId,
FirstName = e.FirstName,
LastName = e.LastName,
Specialties = specialties.Where(s => eBridge.Contains(s.Id)).ToList()
});

C# link how to populate parent list with sublist from two related lists

I'm new to Linq but need to get some code finished quickly. I have two classes:
public class SAPMember
{
public string EmployeeNumber { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<CostCentre> CostCentres { get; set; }
}
public class CostCentre
{
public string EmployeeNumber { get; set; }
public string CostCentreCode { get; set; }
public string Division { get; set; }
}
Each SAPMember can have one or more CostCentres.
This is my code to populate a two independent lists initially (using Dapper), which I then hope to combine into one list with a sublist:
_SAPMembers = new List<SAPMember>();
string sql = #"SELECT EmployeeNo as EmployeeNumber,
LastName, FirstName
FROM Employees";
_SAPMembers = DbConn.Query<SAPMember>(sql).ToList();
List<CostCentre> _CostCentres = new List<CostCentre>();
string sql2 = #"SELECT EmployeeNo as EmployeeNumber, CostCentreCode,
DivisionDescription as Division
FROM Employees";
_CostCentres = DbConn.Query<CostCentre>(sql2).ToList();
I've tried Linq grouping and joins etc but can't get the syntax right, and my _SAPMembers list populated with employee details plus related list of costcentres.
Code sample would be greatly appreciated. I've seen that this might be possible from one more complex Dapper query, but I think for my skill level linq might be a better solution.
As Amit suggested, I could use Dapper, as so: (note:I changed CostCentre key to EmpNo)
if (_SAPMembers == null)
{
List _SAPMembers = new List();
var lookup = new Dictionary<string, SAPMember>();
var result = DbConn.Query<SAPMember, CostCentre, SAPMember>(#"
select DISTINCT e.EmployeeNo as EmployeeNumber, e.LastName, e.FirstName, c.EmployeeNo as EmpNo
, c.CostCentreCode as Name, c.DivisionDescription as Division
FROM EmployeeListing e
join EmployeeListing c on e.EmployeeNo = c.EmployeeNo
where e.TerminationDate is null and c.TerminationDate is null", (e, c) =>
{
if (!lookup.TryGetValue(e.EmployeeNumber, out SAPMember sapMember))
lookup.Add(e.EmployeeNumber, sapMember = e);
if (sapMember.CostCentres == null)
sapMember.CostCentres = new List<CostCentre>();
sapMember.CostCentres.Add(c);
return sapMember;
}, splitOn: "EmpNo");
_SAPMembers = result.Distinct().ToList();
}
return _SAPMembers;

Adding Linq method with column rename results to viewModel

I have no problems passing Linq query results to views via a viewmodel with or without a .Select() method as long as I am selecting only a single column. When I tried using the .Select() option with a renamed column like this:
var custodians = _custodian.Contacts
.Where(c => !(c.personid.StartsWith("RMR") || c.personid.StartsWith("GMS")))
.Select(c => new { c.contactid, name = c.lname + ", " + c.fname})
.ToList();
it creates creates a System.Collections.Generic.List<<>f__AnonymousType1<int, string>> type list
I have an existing viewModel that I am passing to my view:
public class AssetViewModel
{
public string PsgcTagNumber { get; set; }
public string[] AssetAttributes { get; set; }
public string Message { get; set; }
public Asset Asset { get; set; }
public Location Location { get; set; }
public string Custodian { get; set; }
public ?????? AllContacts { get; set; }
}
What I cant figure out is the datatype to use for the AllContacts property of the viewModel.
Anyone point me in the right direction?
You'll need to define a class.
public class Contact {
public int contactid {get;set;}
public string name {get;set;}
}
.Select(c => new Contact { contactid = c.contactid, name = c.lname + ", " + c.fname})
public Contact[] AllContacts { get; set; }
Or just leave the entity alone, without doing a Select method on your query, and use it in your viewmodel - you could add a FormattedName property or something like that to handle your name.
Your anonymous type result is exactly what your select is producing new { c.contactid, name = c.lname + ", " + c.fname} - a list of int<->string or a list of { int contactid, string name }
If you want to use an existing Model, like your AssetViewModel.AllContacts you need to define its type first, as #Joe Enos stated and then update your query a little bit:
var vm = new AssetViewModel
{
PsgcTagNumber =...,
...,
Custodian =...,
AllContacts = _custodian.Contacts
.Where(c => !(c.personid.StartsWith("RMR") || c.personid.StartsWith("GMS")))
.Select(c => new Contact { c.contactid, name = c.lname + ", " + c.fname})
.ToList();
}
So then you have it: your view model, initiated and ready to be passed forward

Convert simple Left Outer Join and group by SQL statement into Linq

2 tables: User and Alarm
Table:User
UserID(int),
FullName(varchar)
Table:Alarm
AssignedTo(int),
Resolved(bool)
Query:
SELECT u.Fullname, COUNT(resolved) as Assigned, SUM(CONVERT(int,Resolved)) as Resolved, COUNT(resolved) - SUM(CONVERT(int,Resolved)) as Unresolved
FROM Alarm i LEFT OUTER JOIN Users u on i.AssignedTo = u.UserID
GROUP BY u.Fullname
Results:
Fullname Assigned Resolved Unresolved
User1 204 4 200
User2 39 9 30
User3 235 200 35
User4 1 0 1
User5 469 69 400
For the life of me I can't figure out how to make this into a Linq query. I am having trouble with the grouping function.
I've looked a countless examples and none have my combination of Left Outer join with grouping or they are so complicated that I can't figure out how to make it work with mine. Any help here would be Greatly appreciated!!!
Update:
I may not have been clear in what I'm looking for. I am looking for the alarms grouped by the AssignedTo Column which is a userid... Except, I want to replace that userid with the FullName that is located in the users table. Someone had posted and deleted something close except it gave me all users in the user table which is not what I'm looking for..
Update 2: See my answer below
Assuming that you have the following models:
This is the model for Alarm:
public class Alarm
{
public int id { get; set; }
public int AssignedTo { get; set; }
[ForeignKey("AssignedTo")]
public virtual User User { get; set; }
public bool Resolved { get; set; }
}
This is the model for User:
public class User
{
public int UserID { get; set; }
public string FullName { get; set; }
public virtual ICollection<Alarm> Alarms { get; set; }
public User()
{
Alarms = new HashSet<Alarm>();
}
}
This is the model that will hold the alarm statistics for each user:
public class UserStatistics
{
public string FullName { get; set; }
public int Assigned { get; set; }
public int Resolved { get; set; }
public int Unresolved { get; set; }
}
You can then do the following:
var query = context.Users.Select(
user =>
new UserStatistics
{
FullName = user.FullName,
Assigned = user.Alarms.Count,
Resolved = user.Alarms.Count(alarm => alarm.Resolved),
Unresolved = user.Alarms.Count(alarm => !alarm.Resolved)
});
var result = query.ToList();
By the way, you can also modify the query and remove Unresolved = user.Alarms.Count(alarm => !alarm.Resolved), and then make the Unresolved property a calculated property like this:
public class UserStatistics
{
public string FullName { get; set; }
public int Assigned { get; set; }
public int Resolved { get; set; }
public int Unresolved
{
get { return Assigned - Resolved; }
}
}
This will make the generated SQL query simpler.
I finally figured it out.
This:
var results = alarms.GroupBy(x => x.AssignedTo)
.Join(users, alm => alm.Key , usr => usr.UserID, (alm, usr) => new {
Fullname = usr.FullName,AssignedNum = alm.Count(),
Resolved = alm.Where(t=>t.resolved == true).Select(y => y.resolved).Count(),
Unresolved = alm.Where(t=>t.resolved == false).Select(y => y.resolved).Count() });
Reproduces This:
SELECT u.Fullname, COUNT(resolved) as Assigned, SUM(CONVERT(int,Resolved)) as Resolved,
COUNT(resolved) - SUM(CONVERT(int,Resolved)) as Unresolved
FROM Alarm i LEFT OUTER JOIN Users u on i.AssignedTo = u.UserID
GROUP BY u.Fullname
The result is grouped by the AssignedTo (int) but AssignedTo is not selected. Instead FullName is selected from the joined user table.
Many thanks to everyone that tried to help! I learned a lot from your answers.
For bonus points, how would I write my lamdbda answer in a SQL like syntax?
Try this :
from u in context.User
join a in context.Alarm on u.UserID equals a.AssignedTo into g1
from g2 in g1.DefaultIfEmpty()
group g2 by u.Fullname into grouped
select new { Fullname = grouped.Key, Assigned = grouped.Count(t=>t.Resolved != null), Resolved = grouped.Sum
(t => int.Parse(t.Resolved)), Unresolved = (grouped.Count(t=>t.Resolved != null) - grouped.Sum
(t => int.Parse(t.Resolved)))}
I guess it is not necessarily to use "Grouping" for this query in Linq because the combination of "LEFT JOIN" + "GROUP BY" changed them over to "INNER JOIN".
var results =
from u in users
join a in alarms on u.UserID equals a.AssignedTo into ua
select new
{
Fullname = u.FullName,
Assigned = ua.Count(),
Resolved = ua.Count(a => a.Resolved),
Unresolved = ua.Count(a => !a.Resolved)
};
foreach (var r in results)
{
Console.WriteLine(r.Fullname + ", " + r.Assigned + ", " + r.Resolved + ", " + r.Unresolved);
}

Entity Framework Traverse and return child records in self reference table

I am using Entity Framework and have a table of BusinessUnits which can reference another record of the same type to form a child-parent hierarchy.
I also have a set of users, and user permissions, where each user defined in this table should have access to the BusinessUnit and all sub-business units in the hierarchy. Users should not have access to the BusinessUnit above the one referenced (if exists).
How can I go about forming LINQ queries to handle this self-referencing relationship tree and return all the business units (with child units) for which this user has access? Is it possible to do it in one query, or do I need to manually build the tree myself with a for-loop?
I have seen schema's reference in this way from node to parent, does this mean I have to start at the furthest child node to build the tree by one parent at a time?
Thanks in advance,
Chris
class BusinessUnit
{
int BusinessUnitID {get;set;}
public string BusinessName {get;set;}
BusinessUnit ParentBusinessUnit {get;set;}
}
class User
{
int UserID {get;set;}
string Firstname {get;set;}
}
class UserPermissions
{
[Key, ForeignKey("BusinessUnit"), Column(Order = 0)]
BusinessUnit BusinessUnit {get;set;}
[Key, ForeignKey("User"), Column(Order = 1)]
User User {get;set;}
}
IEnumerable<BusinessUnit> GetUnitsForWhichUserHasAccess(User user)
{
/* Example 1
given: BusinessUnitA (ID 1) -> BusinessUnitB (ID 2) -> BusinessUnitC (ID 3)
with user with ID 1:
and UserPermissions with an entry: BusinessUnit(2), User(1)
the list { BusinessUnitB, BusinessUnitC } should be returned
*/
/* Example 2
given: BusinessUnitA (ID 1) -> BusinessUnitB (ID 2) -> BusinessUnitC (ID 3)
with user with ID 1:
and UserPermissions with an entry: BusinessUnit(1), User(1)
the list { BusinessUnitA, BusinessUnitB, BusinessUnitC } should be returned
*/
}
OK, there are a few things here. We can make this a bit easier by adding some more properties to your model. Is that an option? If so, add collection properties to the entities. Now, I don't know which EF API you're using: DbContext (code first or edmx) or ObjectContext. In my sample I've used the DbContext API with an edmx model to generate these classes.
If you prefer, with a few annotations you could dispense with the edmx file.
public partial class BusinessUnit
{
public BusinessUnit()
{
this.ChlidBusinessUnits = new HashSet<BusinessUnit>();
this.UserPermissions = new HashSet<UserPermissions>();
}
public int BusinessUnitID { get; set; }
public string BusinessName { get; set; }
public int ParentBusinessUnitID { get; set; }
public virtual ICollection<BusinessUnit> ChlidBusinessUnits { get; set; }
public virtual BusinessUnit ParentBusinessUnit { get; set; }
public virtual ICollection<UserPermissions> UserPermissions { get; set; }
}
public partial class User
{
public User()
{
this.UserPermissions = new HashSet<UserPermissions>();
}
public int UserID { get; set; }
public string FirstName { get; set; }
public virtual ICollection<UserPermissions> UserPermissions { get; set; }
}
public partial class UserPermissions
{
public int UserPermissionsID { get; set; }
public int BusinessUnitID { get; set; }
public int UserID { get; set; }
public virtual BusinessUnit BusinessUnit { get; set; }
public virtual User User { get; set; }
}
public partial class BusinessModelContainer : DbContext
{
public BusinessModelContainer()
: base("name=BusinessModelContainer")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public DbSet<BusinessUnit> BusinessUnits { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<UserPermissions> UserPermissions { get; set; }
}
#Chase medallion is correct in that we can't write recursive LINQ (or even Entity SQL) queries.
Option 1: Lazy Loading
With lazy loading enabled, you could do something like this...
private static IEnumerable<BusinessUnit> UnitsForUser(BusinessModelContainer container, User user)
{
var distinctTopLevelBusinessUnits = (from u in container.BusinessUnits
where u.UserPermissions.Any(p => p.UserID == user.UserID)
select u).Distinct().ToList();
List<BusinessUnit> allBusinessUnits = new List<BusinessUnit>();
foreach (BusinessUnit bu in distinctTopLevelBusinessUnits)
{
allBusinessUnits.Add(bu);
allBusinessUnits.AddRange(GetChildren(container, bu));
}
return (from bu in allBusinessUnits
group bu by bu.BusinessUnitID into d
select d.First()).ToList();
}
private static IEnumerable<BusinessUnit> GetChildren(BusinessModelContainer container, BusinessUnit unit)
{
var eligibleChildren = (from u in unit.ChlidBusinessUnits
select u).Distinct().ToList();
foreach (BusinessUnit child in eligibleChildren)
{
yield return child;
foreach (BusinessUnit grandchild in child.ChlidBusinessUnits)
{
yield return grandchild;
}
}
}
Option 2: Pre-load Entities
However, there are some ways you could optimize this to avoid repeated trips to the server. If you have only a reasaonably small number of business units in the database you could load the entire list. Then, because of EFs ability to fix up relationships automatically, simply loading a user and his permissions from the database would give us all we need.
To clarify: this method means that you load all the BusinessUnit entities; even the ones that the user has no permissions to. However, because it greatly reduces the 'chatter' with the SQL Server, it may still perform better than Option 1 above. Unlike Option 3 below, this is 'pure' EF without any dependency on a specific provider.
using (BusinessModelContainer bm = new BusinessModelContainer())
{
List<BusinessUnit> allBusinessUnits = bm.BusinessUnits.ToList();
var userWithPermissions = (from u in bm.Users.Include("UserPermissions")
where u.UserID == 1234
select u).Single();
List<BusinessUnit> unitsForUser = new List<BusinessUnit>();
var explicitlyPermittedUnits = from p in userWithPermissions.UserPermissions
select p.BusinessUnit;
foreach (var bu in explicitlyPermittedUnits)
{
unitsForUser.Add(bu);
unitsForUser.AddRange(GetChildren(bm, bu));
}
var distinctUnitsForUser = (from bu in unitsForUser
group bu by bu.BusinessUnitID into q
select q.First()).ToList();
}
Please note that the above two examples could be improved, but serve as an example to get you going.
Option 3: Bespoke SQL Query Using Common Table Expression
If you have a large number of business units, you might want to try the most efficient method. That would be to execute custom SQL that uses a hierarchical Common Table Expression to get back the information in one hit. This would of course tie the implementation to one provider, probably SQL Server.
Your SQL would be something like this:
WITH UserBusinessUnits
(BusinessUnitID,
BusinessName,
ParentBusinessUnitID)
AS
(SELECT Bu.BusinessUnitId,
Bu.BusinessName,
CAST(NULL AS integer)
FROM Users U
INNER JOIN UserPermissions P ON P.UserID = U.UserID
INNER JOIN BusinessUnits Bu ON Bu.BusinessUnitId = P.BusinessUnitId
WHERE U.UserId = ?
UNION ALL
SELECT Bu.BusinessUnitId,
Bu.BusinessName,
Bu.ParentBusinessUnitId
FROM UserBusinessUnits Uu
INNER JOIN BusinessUnits Bu ON Bu.ParentBusinessUnitID = Uu.BusinessUnitId)
SELECT DISTINCT
BusinessUnitID,
BusinessName,
ParentBusinessUnitID
FROM UserBusinessUnits
You would use code like the following to materialize a collection of BusinessUnit objects for which the user has permissions.
bm.BusinessUnits.SqlQuery(mySqlString, userId);
There is a subtle difference between the above line and the very similar code suggested by #Jeffrey. The above uses DbSet.SqlQuery() while his uses Database.SqlQuery. The latter produces entities that are not tracked by the context, while the former returns (by default) tracked entities. Tracked entities give you the ability to make and save changes, and automatic fix-up of the navigation properties. If you don't need those features, disable change tracking (either with .AsNoTracking() or by using Database.SqlQuery).
Summary
Nothing beats testing with a realistic data set to determine which method is most effective. Using hand-crafted SQL code (Option 3) is always likely to perform best, but at the expense of having more complicated code that is less portable (because it's tied to the underlying db technology).
Note also that the options available to you depend on the "flavour" of EF that you're using, and of course, on your chosen database platform. If you would like some more specific guidance that accounts for this please update your question with the extra information.
What database do you use?
Does you project use an EDMX file, or code first?
If using an EDMX, do you use the default (EntityObject) code generation technique, or T4 templates?
If I'm understanding correctly, what you want is a recursive query (a recursive common table expression in raw T-SQL). As far as I know, there is no way to write such a recursive query in pure LINQ to Entities.
However, if you know the max depth the hierarchy, you can build up a single query that joins on itself a fixed number of times to achieve the result that you want.
int userIdOfInterest = ...
IQueryable<BusinessUnit> units = ...
// start with a query of all units the user has direct permission to
var initialPermissionedUnits = units.Where(bu => bu.UserPermissions.Any(up => up.User.Id == userIdOfInterest));
var allHierarchyLevels = new Stack<IQueryable<BusinessUnit>();
allHierarchyLevels.Push(initialPermissionedUnits);
for (var i = 0; i < MAX_DEPTH; ++i) {
// get the next level of permissioned units by joining the last level with
// it's children
var nextHierarchyLevel = allHierarchyLevels.Peek()
// if you set up a Children association on BusinessUnit, you could replace
// this join with SelectMany(parent => parent.Children)
.Join(units, parent => parent.BusinessUnitId, child => child.ParentBusinessUnit.BusinessUnitId, (parent, child) => child));
allHierarchyLevels.Push(nextHierarchyLevel);
}
// build an IQueryable<> which represents ALL units the query is permissioned too
// by UNIONING together all levels of the hierarchy (the UNION will eliminate duplicates as well)
var allPermissionedUnits = allHierarchyLevels.Aggregate((q1, q2) => q1.Union(q2));
// finally, execute the big query we've built up
return allPermissionedUnits.ToList();
Of course, the performance of the generated query will most likely worsen as MAX_DEPTH increases. However, it will probably be better that executing 1 query per level of the hierarchy in a for loop.
If you don't know MAX_DEPTH, you could consider adding a depth column to your business units table (easy to set on insertion because it's always parent.depth + 1). Then you could easily query for MAX_DEPTH before running the permissioning query.
If you're not tied to using linq for the solution, its vastly simpler and faster to use a CTE in sql as such:
var sql = #"
WITH BusinessUnitHierarchy ( BusinessUnitID, BusinessName, ParentBusinessUnitID )
AS(
Select bu.BusinessUnitID, bu.BusinessName, bu.ParentBusinessUnitID
from BusinessUnit bu
inner join [UserPermissions] up on bu.BusinessUnitID = up.BusinessUnitID
where up.UserID = #userID
UNION ALL
Select
bu.BusinessUnitID, bu.BusinessName, bu.ParentBusinessUnitID
from BusinessUnit bu
inner join BusinessUnitHierarchy buh on bu.ParentBusinessUnitID = buh.BusinessUnitID
)
SELECT * FROM BusinessUnitHierarchy buh
";
context.Database.SqlQuery<BusinessUnit>(sql, new SqlParameter("userID", [[your user ID here]]));
Recursive CTE in SQL is just a technique using ground rules. You can build the same query in LINQ using those ground rules.
Here are the simple steps to follow
1) Get the list of permissions from UserPermissions table
2) Foreach permission, recurse the tree to find the subset of permission
There are lot of ways to optmize\adapt these queries but here is the core:
//Gets the list of permissions for this user
static IEnumerable<BusinessUnit> GetPermissions(int userID)
{
//create a permission tree result set object
List<BusinessUnit> permissionTree = new List<BusinessUnit>();
//Get the list of records for this user from UserPermissions table
IEnumerable<UserPermissions> userPermissions = from UP in UPs
where UP.User.UserID == userID
select UP;
//for each entry in UserPermissions, build the permission tree
foreach (UserPermissions UP in userPermissions)
{
BuildPermissionTree(UP.BusinessUnit, permissionTree);
}
return permissionTree;
}
//recursive query that drills the tree.
static IEnumerable<BusinessUnit> BuildPermissionTree(BusinessUnit pBU,List<BusinessUnit> permissionTree)
{
permissionTree.Add(pBU);
var query = from BU in BUs
where BU.ParentBusinessUnit == pBU
select BU;
foreach (var BU in query)
{
BuildPermissionTree(BU,permissionTree);
}
return permissionTree;
}
O\p when queried for User 1 -> Permissions in (B,C) (refer diagram)
BusinessUnitB
BusinessUnitG
BusinessUnitC
BusinessUnitD
BusinessUnitF
BusinessUnitE
Here is the full code :
class BusinessUnit
{
public int BusinessUnitID { get; set; }
public string BusinessName { get; set; }
public BusinessUnit ParentBusinessUnit { get; set; }
public override string ToString()
{
return BusinessUnitID + " " + BusinessName + " " + ParentBusinessUnit;
}
}
class User
{
public int UserID { get; set; }
public string Firstname { get; set; }
public override string ToString()
{
return UserID + " " + Firstname;
}
}
class UserPermissions
{
public BusinessUnit BusinessUnit { get; set; }
public User User { get; set; }
public override string ToString()
{
return BusinessUnit + " " + User;
}
}
class SOBUProblem
{
static List<BusinessUnit> BUs = new List<BusinessUnit>();
static List<User> Users = new List<User>();
static List<UserPermissions> UPs = new List<UserPermissions>();
static void Main()
{
//AutoInitBU();
InitBU();
InitUsers();
InitUPs();
//Dump(BUs);
//Dump(Users);
//Dump(UPs);
//SpitTree(BUs[2]);
int userID = 1;
foreach (var BU in GetPermissions(userID))
Console.WriteLine(BU.BusinessName);
}
//Gets the lsit of permissions for this user
static IEnumerable<BusinessUnit> GetPermissions(int userID)
{
//create a permission tree result set object
List<BusinessUnit> permissionTree = new List<BusinessUnit>();
//Get the list of records for this user from UserPermissions table
IEnumerable<UserPermissions> userPermissions = from UP in UPs
where UP.User.UserID == userID
select UP;
//for each entry in UserPermissions, build the permission tree
foreach (UserPermissions UP in userPermissions)
{
BuildPermissionTree(UP.BusinessUnit, permissionTree);
}
return permissionTree;
}
//recursive query that drills the tree.
static IEnumerable<BusinessUnit> BuildPermissionTree(BusinessUnit pBU,List<BusinessUnit> permissionTree)
{
permissionTree.Add(pBU);
var query = from BU in BUs
where BU.ParentBusinessUnit == pBU
select BU;
foreach (var BU in query)
{
BuildPermissionTree(BU,permissionTree);
}
return permissionTree;
}
static void Dump<T>(IEnumerable<T> items)
{
foreach (T item in items)
{
Console.WriteLine(item.ToString());
}
}
static void InitBU()
{
BusinessUnit BURoot = new BusinessUnit() { BusinessUnitID = 1, BusinessName = "BusinessUnitA" };
BUs.Add(BURoot);
BusinessUnit BUlevel11 = new BusinessUnit() { BusinessUnitID = 2, BusinessName = "BusinessUnitB", ParentBusinessUnit = BURoot };
BusinessUnit BUlevel12 = new BusinessUnit() { BusinessUnitID = 3, BusinessName = "BusinessUnitC", ParentBusinessUnit = BURoot };
BUs.Add(BUlevel11);
BUs.Add(BUlevel12);
BusinessUnit BUlevel121 = new BusinessUnit() { BusinessUnitID = 4, BusinessName = "BusinessUnitD", ParentBusinessUnit = BUlevel12 };
BusinessUnit BUlevel122 = new BusinessUnit() { BusinessUnitID = 5, BusinessName = "BusinessUnitE", ParentBusinessUnit = BUlevel12 };
BUs.Add(BUlevel121);
BUs.Add(BUlevel122);
BusinessUnit BUlevel1211 = new BusinessUnit() { BusinessUnitID = 6, BusinessName = "BusinessUnitF", ParentBusinessUnit = BUlevel121 };
BUs.Add(BUlevel1211);
BusinessUnit BUlevel111 = new BusinessUnit() { BusinessUnitID = 7, BusinessName = "BusinessUnitG", ParentBusinessUnit = BUlevel11 };
BUs.Add(BUlevel111);
}
static void AutoInitBU()
{
BusinessUnit BURoot = new BusinessUnit() { BusinessUnitID = 1, BusinessName = "BusinessUnitA" };
BUs.Add(BURoot);
Dictionary<int, string> transTable = new Dictionary<int, string>() {{2,"B"},{3,"C"} };
//Create Child nodes
for (int i = 0; i < 2; i++)
{
BUs.Add(new BusinessUnit() { BusinessUnitID = i + 2, BusinessName = "BusinessUnit" + transTable[i+2],ParentBusinessUnit = BUs[i]});
}
}
static void InitUsers()
{
Users.Add(new User() {UserID = 1,Firstname="User1" });
}
static void InitUPs()
{
UPs.Add(new UserPermissions() { BusinessUnit = BUs[1], User = Users[0] });
UPs.Add(new UserPermissions() { BusinessUnit = BUs[2], User = Users[0] });
}
}
To get hierarchy in single request, you need to use special table structure. One of possible solutions is to have a special key that contains all parents of this record. In this case you have simple and very fast (it will be faster than cte recursion) query to get all childs.
But if you want to move record to another branch of hierarchy it will be very expansive operation.
I had to solve a problem of returning hierarchal json data to the web and I started off by using Olly suggestion of using Common Expression table (CET) and my code was
static public IEnumerable<TagMaster> GetHierarchy(IEnumerable<int> surveyId, Entities dbContext)
{
var sql = String.Format( #"
WITH SurveyTags ([TagID], [TagTitle], [SurveyID], [ParentTagID]) AS (
SELECT [TagID], [TagTitle], [SurveyID], [ParentTagID]
FROM [dbo].[TagMaster]
WHERE [SurveyID] in ({0}) and ParentTagID is null
UNION ALL
SELECT
TagMaster.[TagID], TagMaster.[TagTitle], TagMaster.[SurveyID], TagMaster.[ParentTagID]
FROM [dbo].[TagMaster]
INNER JOIN SurveyTags ON TagMaster.ParentTagID = SurveyTags.TagID
)
SELECT [TagID], [TagTitle], [SurveyID], [ParentTagID]
FROM SurveyTags", String.Join(",", surveyId));
return dbContext.TagMasters.SqlQuery(sql).Where(r => r.ParentTagID == null).ToList();
}
But I noticed when accessing the children, the web app was still making round trips to the database! It is also painful to just pass the Entity object to Json because you many end up with fields you don’t want.
The final solution I came up with does not need CET and only makes one trip to the DB. In my case, I could pull up all the records based on the SurveyId but if you don’t have such a key to use, you can still use the CET to get the hierarchy.
This is how I converted the flat records to a tree and just took the fields I need.
1) First load the records I need from the db.
var tags = db.TagMasters.Where(r => surveyIds.Contains(r.SurveyID)).Select(r => new { id = r.TagID, name = r.TagTitle, parentId = r.ParentTagID }).ToList();
2) Create a dictionary of ViewModels for it.
var tagDictionary = tags.Select(r => new TagHierarchyViewModel { Id = r.id, Name = r.name }).ToDictionary(r => r.Id);
3) Then convert it to a hierarchy.
foreach (var tag in tags) {
if (tag.parentId.HasValue) {
tagDictionary[tag.parentId.Value].Tags.Add(tagDictionary[tag.id]);
}
}
4) Remove all the child nodes.
var tagHierarchy = from td in tagDictionary
join t in tags on td.Key equals t.id
where t.parentId == null
select td.Value;
Result:

Categories

Resources