Selecting parents recursively (for ACL/Permissions system) - c#

Important things
Model Category:
[Table("Category")]
public class Category
{
public Category()
{
Children = new HashSet<Category>();
}
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("Parent")]
public int? ParentId { get; set; }
[ForeignKey("ParentId")]
public virtual Category Parent { get; set; }
public virtual ICollection<Category> Children { get; set; }
public string Name { get; set; }
}
Database entries:
---------------------------------
| Id | Parent | Name |
---------------------------------
| 1 | NULL | Top Category |
---------------------------------
| 2 | 1 | Second Category |
---------------------------------
| 3 | 2 | Third Category |
---------------------------------
| 4 | 3 | Fifth Category |
---------------------------------
And now the problem...
How i use a query like...
SELECT "Name" FROM "Category" WHERE "Parent" IS NULL;
with a query like...
SELECT "c"."Id", "c"."Parent", "p"."Name" FROM "Category" "c" LEFT JOIN "Category" "p" ON "p"."Id" = "c"."Parent" WHERE "c"."Name" LIKE '%d%';
I want to select a category with all their parents recursively.
It would be really nice to have a LINQ query for that.
Expected result is something like this:
----------------------------------------------
| "c"."Id" | "c"."Parent" | "p"."Name" |
----------------------------------------------
| 2 | 1 | Top Category |
----------------------------------------------
| 3 | 2 | Second Category |
----------------------------------------------
Or with a group?
-----------------------------------------------------------
| "c"."Id" | "c"."Parent" | GROUP_CONCAT(...) |
-----------------------------------------------------------
| 2, 3 | 1, 2 | Top Category, Second Category |
-----------------------------------------------------------
The category "system" above is a example for a bigger plan...:
( Is there a framework / nuget plugin which allow to use ACL/Permissions/Rights on objects which stored in database?
I have users and groups. Then i have folders and files. Each user or group can have rights for specific folder or file. You can enable heredity for file oder folder-permissions. And with this query i try to read all permissions for a user and a specific folder with all their parents... )

Try this query
var result =
from first in db.Category
join second in db.Category on first.Id equals second.ParentId.GetValueOrDefault(0)
where first.name.Contains(parameter)
select new
{
Id = first.Id ,
ParentId= second.ParentId ?? 0,
ParentName= second.Name
};
I have used second.ParentId.GetValueOrDefault(0) because the join is made on two differents types int and nullable of int

Related

How to Join or search Id with one column which have some Id

I have two tables:
Student Conference
+----+------+ +----+-------+---------------+
| id | name | | id | name | ref_studentId |
+----+------+ +----+-------+---------------+
| 1 | jack | | 1 | Rose | 1,12 |
| 2 | kai | | 2 | White | 12 |
| 3 | wind | | 3 | Black | 1,12,12356 |
+----+------+ +----+-------+---------------+
And I want to join them together with studentid or search for studentid in conference table.
You can use the LINQ Join method to achieve what you want:
var result = dataContext.Students.Join(dataContext.Conferences,
st => st.id,
cf => cf.ref_studentId,
(student, conference) => new { ... });
Although I would strongly recommend using the Entity Framework Navigation Properties, using which the above can be done more easily:
var result = dataContext.Student.Include(st => st.conference);
Update:
Please note that the above LINQ Query will fail to execute unless you will fix your Conference Table design, which is against SQL Normalization Forms (specifically the 1st Normal Form), saying that:
Each sell should be single-valued.
That means that you shouldn't have a comma (or any other character) separated values in your table columns.
To make the above query work, you have to make the ref_studentId contain only a single value for the StudentId:
Conference
+----+--------+-----------+
| ID | Name | StudentId |
+----+--------+-----------+
| 1 | Rose | 1 |
| 2 | Rose | 12 |
| 3 | White | 12 |
| 4 | Black | 1 |
| 5 | Black | 12 |
| 6 | Black | 12356 |
+----+--------+-----------+
But to be honest, this one too will not match the SQL Normalization rules.
Specifically, the 2nd Normal Form, saying that:
All Attributes (non-key columns) should be dependent on a Key.
And the 4th Normal form, saying that:
There should be no multi-value dependencies (Rose ⟶ 1, Rose ⟶ 12)
The correct solution would be to create another table for the Student⟶Conference relations.
Conference ConferencesStudents
+----+-------+ +----+--------------+-----------+
| ID | Name | | ID | ConferenceID | StudentId |
+----+-------+ +----+--------------+-----------+
| 1 | Rose | | 1 | 1 | 1 |
| 2 | White | | 2 | 1 | 12 |
| 3 | Black | | 3 | 2 | 12 |
+----+-------+ | 4 | 3 | 1 |
| 5 | 3 | 12 |
| 6 | 3 | 12356 |
+----+--------------+-----------+
Now, for this one the LINQ query will be:
dataContext.ConferencesStudents.Join(dataContext.Students,
cf => cf.ConferenceID,
st => st.ID,
(conferencesStudents, student) => new { conferencesStudents, student })
.Join(dataContext.Conferences,
cfSt => cfSt.conferencesStudents.ConferenceID,
cf => cf.ID,
(cfSt, conference) =>
new
{
Student = cfSt.student,
Conference = conference
});
Note: for the above, I've used the Anonymous Types just for demonstration and my strong advice would be to use real class Models instead.
OR
By using the same Navigation Properties (in case if you correctly had defined the EF relations) you can have a simpler version:
dataContext.ConferenceStudents.Include(cf => cf.Conferences)
.Include(cf => cf.Students)
Update 2:
I hate to say it but there is a kind of workaround in case if you can't change the Database design:
var results = (
from c in dataContext.Conference.ToList() // populating all Conferences
from s in dataContext.Students.ToList() // populating all Students
where c.ref_studentId.Split(',').Contains(s.id.ToString())
select new
{
Student = s,
Conference = c
}).ToList();
Note: this WILL NOT BE EFFECTIVE from the application performance perspective.
The better alternative compared to the above could be to write Stored Procedure and call it from the EF.
I have prepared a fiddle for you - if there are any questions feel free to ask.
Btw: This solution solves your question. The data-structure itself is ugly and should be normalized.
void Main()
{
var students = new List<Student>
{
new Student { Id = 1, Name = "jack" },
new Student { Id = 12, Name = "kai" },
new Student { Id = 12356, Name = "wind" }
};
var conferences = new List<Conference>
{
new Conference { Id = 1, Name = "Rose", RefIds = "1,12" },
new Conference { Id = 2, Name = "White", RefIds = "12" },
new Conference { Id = 25, Name = "Black", RefIds = "1,12,12356" }
};
var result = students.Select(s => new Tuple<int, Conference[]>(s.Id, conferences.Where(c => c.RefIds.Split(",").Contains(s.Id.ToString())).ToArray()));
}
// Define other methods, classes and namespaces here
public class Student
{
public int Id {get; set;}
public string Name {get;set;}
}
public class Conference
{
public int Id {get; set;}
public string Name {get; set;}
public string RefIds {get; set;}
}

Duplicate fields name in join result in Dapper + Oracle in C#

I'm doing test in Dapper + Oracle in C#
the sample tables as below
Book
+----+----------------+-----------+
| id | title | author_id |
+----+----------------+-----------+
| 1 | this is a book | 2 |
+----+----------------+-----------+
Author
+----+------+
| id | name |
+----+------+
| 2 | Mark |
+----+------+
my code:
string queryString = "SELECT distinct * FROM book b INNER JOIN author a ON a.id = b.author_id";
var result = connection.Query(queryString);
the output:
{DapperRow, id = 1, title='this is a book', author_id = 2, id = 2, name = 'Mark'}
there are two fields with duplicate name "id" in result
how could I add alias or postfix to the duplicate fields like the output in sqldeveloper like below
+----+----------------+-----------+------+------+
| id | title | author_id | id_1 | name |
+----+----------------+-----------+------+------+
| 1 | this is a book | 2 | 2 | Mark |
+----+----------------+-----------+------+------+
thanks
Use aliases in select statement:
string queryString = "SELECT distinct b.id [book_id], ... FROM book b INNER JOIN author a ON a.id = b.author_id";

Get details by joining tables with group by condition in Linq and EF

I have the following 3 table definitions in my SQL Server database:
+----------------------------------+-----------------+------------------------------------------------
| Product | Rating | Image |
+----------------------------------+-----------------+-------------------------------------------------
| ProductId | Id | Id |
| ProdctName | Rating | Image |
| | ProductId FK References(Product)| ProductId FK References(Product)|
+----------------------------------+-----------------+---------------+----------------------------------
And these tables contain the following sample data:
+----------------------------------+------------
|Product | ProductId |ProductName |
+----------------------------------+------------
| | 1 |Prodcuct 1 |
| | 2 |Prodcuct 2 |
| | 3 |Prodcuct 3 |
| | 4 |Prodcuct 4 |
+----------------------------------+------------+
+----------------------------------+----------------------+
|Rating | Id |RatingVal |ProductId |
|+----------------------------------+-----------------------
| | 1 |3 |1 |
| | 2 |2 |2 |
| | 3 |3 |2 |
| | 4 |5 |3 |
| | 5 |4 |3 |
+---------------------+------------+------------+----------+
+----------------------------------+----------------------+
|Image | Id |ImagePath |ProductId
+----------------------------------+-----------------------
| | 1 |ABC |1 |
| | 2 |XYZ |2 |
| | 3 |LMN |3 |
| | 4 |PQR |4 |
+---------------------+------------+------------+----------+
I need to gather information about a product in one place, such that each product contains the details about product ( from products table), related average rating ( from ratings table)m and the image path for the product ( from Image table). In other words I need the following output:
+----------------------------------+--------------------------------+
|Output | ProductId |ProductName |Avg(Rating)|ImgPath|
+----------------------------------+--------------------------------+
| | 1 |Prodcuct 1 |3 |ABC |
| | 2 |Prodcuct 2 |2.5 |XYZ |
| | 3 |Prodcuct 3 |4.5 |LMN |
| | 4 |Prodcuct 4 |0.0 |PQR |
+----------------------------------+------------+-----------+-------+
I am using Entity Framework to fetch this data, and entities in context class in my code( shown below).
My question is: How do I produce my desired output for all the products.
My code below is not able to get all the data I want. The problem is that the product with id4 is not shown in the result, I assume this is be cause product 4 does not have an entry in the ratings table.
var trendingProducts = (from ratings in entities.Rating
group ratings by new { ratings.productId } into c
join products in entities.Products on c.FirstOrDefault().productId equals products.ProductID
join images in entities.Images on c.Key.productId equals images.ProductId
select new ProductViewModel
{
ProductId = products.ProductId,
ProductName = products.ProductName,
RatingVal = c.Average(l => l.RatingVal) == null ? 0 : c.Average(l => l.Rating),
ImagePath = images.ImagePath,
}).ToList();
So you have a table of Products, a table of Ratings and a table of Images.
Every Product has zero or more Ratings, every Rating belongs to exactly one Product using foreign key ProductId. Similarly, every Product has zero or more Images, every Image belongs to exactly one Image, using foreign key ProductId. Just standard one-to-many relations.
It might be that every Product has zero-or-one Image: in that case you have a zero-or-one-to-one relation. The code will be similar. The main difference is that 'Product' won't have a collection of Images, it will only have one virtual Image.
If you follow the entity framework code first conventions you'll have classes like:
class Product
{
public int Id {get; set;}
// every Product has zero or more Ratings:
public virtual ICollection<Rating> Ratings {get; set;}
// every product has zero or more Images:
public virtual ICollection<Image> Images {get; set;}
... // other properties
}
class Rating
{
public int Id {get; set;}
// every Rating belongs to exactly one Product using foreign key:
public int ProductId {get; set;}
public virtual Product Product {get; set;}
...
}
class Image
{
public int Id {get; set;}
// every Image belongs to exactly one Product using foreign key:
public int ProductId {get; set;}
public virtual Product Product {get; set;}
...
}
This is everything entity framework needs to know to detect your one-to-many relations. It knows which tables / columns you want, and it knows the relations between the tables. It might be that you want different identifiers for your tables or properties. In that case you'll need attributes or fluent API. But that is out of scope of this question.
Note that in entity framework all non-virtual properties will become columns in your tables. All virtual properties represent the relations between the tables.
I need to gather information about a product in one place, such that
each product contains the details about product ( from products
table), related average rating ( from ratings table) and the image
path for the product (from Image table).
Whenever people query for "objects with their sub-object" they tend to create a (group)join. However, if you use entity framework it is much easier to use the ICollections for these queries. If you use an ICollection, Entity Framework will know that a (group)join is needed.
var result = myDbContext.Products // from the collection of Products
.Where(product => ...) // select only those Products that ...
.Select(product => new // from every remaining Product make one new
{ // object with the following properties:
// Select only the properties you actually plan to use!
Id = product.Id,
Name = product.ProductName,
...
AverageRating = product.Ratings // from all Ratings of this Product
.Select(rating => rating.Rating) // get the Rating value
.Average(), // and Average it.
Images = product.Images // from all Images of this product
.Select(image => image.ImagePath) // select the ImagePath
.ToList(),
// or if a Product has only zero or one Image:
Image = product.Image?.ImagePath // if the product has an Image, get its ImagePath
// or use null if there is no Image
});
The nice thing about using the ICollections is that the code is simplere and more natural, it looks more similar to the text of your requirement than a (group)join. Besides, if more than two tables are involved (group)joins tend to look horrible.
I won't try to give a solution using a GroupJoin. I'm sure other answers will show this. Compare and see for yourself which solution seems easier to understand.

Linq Account Hierarchy Drill Down

I have two tables, one an invoice table and the other a hierarchy table for the accounts on the invoice
Invoice Table:
| InvoiceNo| AccNo| InvoiceAmount |
--------------------------------------
| A1234| 345| 100.00 |
| A1235| 346| 95.00 |
| A1236| 347| 15.50 |
| A1237| 348| 20.10 |
Hierarchy Table
| AccNo| HierAccNo| Level|
--------------------------------------
| 123| | 1 |
| 789| 123| 2 |
| 890| 123| 2 |
| 345| 789| 3 |
| 346| 789| 3 |
| 347| 890| 3 |
| 348| 890| 3 |
What I'm trying to do is to roll up the amounts from the invoice table to the highest level AccNo which is Level1 and then on a seperate instance from the highest account number roll back down to the next levels.
So far I am able to roll up to the highest band number by the following :
var BandL2 = from invoice in db.Invoices//Roll up to level 2
join ban in db.HierarchyTable
on invoice.AccNo equals ban.Ban
where invoice.GlobalInvoiceID == globalInvoice.Id
group invoice by ban.HierAccNo into bandHierarchy
select new
{
Level2Band = bandHierarchy.Key,
Amount = bandHierarchy.Sum(m=> m.InvoiceAmount)
};
var bandHierarchyTable = db.HierarchyTable.AsQueryable();
var BandL1 = from band2 in BandL2 // Roll Up to level 1
join band1 in bandHierarchyTable
on band2.Level2Band equals band1.Ban
group band2 by band1.HierAccNo into bandL1
select new
{
Level1Band = bandL1.Key,
Amount = bandL1.Sum(m => m.Amount)
};
But now I'm having an issue reversing the process and drilling down from Level 1 as the only details from the form is the AccNo of Level 1(eg. 123).
I'm trying to do this on the fly using pop up modals as I'm drilling.
How do I drill down again so that I can get level by level amounts?
Example:
Ouput Table from Above Code
| AccNo| Amount|
--------------------------------------
| 123| 230.60 |
Then
| AccNo| Amount|
--------------------------------------
| 789| 195 |
| 890| 35.60|
And then clicking on one of the AccNo.
| AccNo| Amount|
--------------------------------------
| 345| 100|
| 346| 95 |
Thanks!
Looks like only leaf accounts can have invoices attached to it, so get the account number, check if it is leaf account, if it is leaf it should be something like that;
var hierAccNo = 123;
var details = from invoice in db.Invoices
join ban in db.HierarchyTable
on invoice.AccNo equals ban.Ban
where invoice.GlobalInvoiceID == globalInvoice.Id
and ban.HierAccNo = HierAccNo;
if it is not leaf you want your original query on level 1 I guess to get the subtotals.
var BandL2 = from invoice in db.Invoices//Roll up to level 2
join ban in db.HierarchyTable
on invoice.AccNo equals ban.Ban
where invoice.GlobalInvoiceID == globalInvoice.Id and ban.HierAccNo = hierAccNo // dont skip this
group invoice by ban.HierAccNo into bandHierarchy
select new
{
Level2Band = bandHierarchy.Key,
Amount = bandHierarchy.Sum(m=> m.InvoiceAmount)
};

LINQ Grouping By with Left Join Results

If I got this list
PersonPhone
------------------------------------------------------------
| **PersonId** |**Name** | **PhoneId** | **PhoneNumber** |
------------------------------------------------------------
| 1 | John Doe | 1 | 111-55-5855 |
------------------------------------------------------------
| 1 | John Doe | 2 | 111-55-5521 |
------------------------------------------------------------
| 2 | Mary Jane| 3 | 254-565-855 |
------------------------------------------------------------
| 3 | J. Watson| NULL| NULL|
------------------------------------------------------------
I need to mapping to this object:
public class PersonContactInfo {
public int Id { get; set; }
public string Name { get; set; }
public List<string> Phones { get; set; }
}
With LINQ, how can I get one row for each person, with his phones in a List and paging?
I already have a query which result is like the PersonPhone result set, but I don't know how to grouping by PersonId and then join all the PhoneNumbers to List, and paging. For example, if I want a page size of three, how to make a sql query to get John Doe, Mary Jane and J. Watson with their phones, if the actual query returns 4 rows?
Note: I'm not (and can't) using Entity Framework, what I'm doing is and sql query that populate a list of PersonPhone, just like EF does.
Applying a group by:
var query= PersonPhoneSet
.GroupBy(p=>new {p.PersonId, p.Name})
.Select(g=> new PersonContactInfo
{
Id=g.Key.PersonId,
Name=g.Key.Name,
Phones= g.Select(p=>p.PhoneNumber).ToList()
}
);

Categories