Better way to update the underlying lookup via EF? - c#

Here's my situation - I've got a DB which has some tables named recipes, ingredients and recipes_ingredients.
Recipes are composed of 1+ ingredients.
The recipes_ingredients has FKs between the recipes and ingredients table.
The classes that get generated are recipe and ingredient and recipe has a navigation property that looks like so:
public virtual ICollection<ingredients> ingredients { get; set; }
Great, I understand that I get a generated recipe class and a generated ingredient class and that the recipes_ingredients table doesn't get a class generated since EF views this simply as a navigation property.
Now, I've got a function called SetIngredientsForRecipe that looks like so (minus the try-catch code for brevity's sake:
public void SetIngredientsForRecipe(long recipeId, List<string> ingredients)
{
using (var db = new FoodEntities(ConnectionString, null, null))
{
var existing = GetCurrentIngredients(recipeId);
var toRemove = existing.Except(ingredients);
var toAdd = ingredients.Except(existing);
var recipe = db.recipes.Where(r => r.Id == recipeId).FirstOrDefault();
foreach (var name in toRemove)
{
var entry = recipe.ingredients.Where(i => i.Name == name).FirstOrDefault();
recipe.ingredients.Remove(entry);
}
foreach (var name in toAdd)
{
var entry = db.ingredients.Where(i => i.Name == name).FirstOrDefault();
recipe.ingredients.Add(entry);
}
db.SaveChanges();
}
}
The intent, as the name suggests, is to update the ingredient list for the given recipe to only whatever is in the list. I'm still getting comfortable with EF and wondering if there's a better (more efficient?) way to accomplish what I'm trying to do.
Follow-up:
Following the suggestions by ntziolis below, I opted to use
recipe.ingredients.Clear() to clear out whatever was in the recipe/ingredient mapping and then use the mocking that was mentioned to quickly add the new ones. Something like this:
foreach (var name in ingredients)
{
// Mock an ingredient since we just need the FK that is referenced
// by the mapping table - the other properties don't matter since we're
// just doing the mapping not inserting anything
recipe.ingredients.Add(new Ingredient()
{
Name = name
});
}
and this works very nicely.

General performance guidelines are:
try to deal with id's only
mock entities whenever possible, rather than retrieving them from db
use the new features of EF4 like Contains in order to simplify and speed up your code
Based on these principles here is a optimized (not simpler though) solution to your problem:
public void SetIngredientsForRecipe(long recipeId, List<string> ingredients)
{
using (var db = new FoodEntities(ConnectionString, null, null))
{
var recipe = db.recipe.Single(r => r.ID == recipeId);
// make an array since EF4 supports the contains keyword for arrays
var ingrArr = ingredients.ToArray();
// get the ids (and only the ids) of the new ingredients
var ingrNew = new HasSet<int>(db.ingrediants
.Where(i => ingrArr.Contains(i.Name))
.Select(i => I.Id));
// get the ids (again only the ids) of the current receipe
var curIngr = new HasSet<int>(db.receipes
.Where(r => r.Id == recipeId)
.SelectMany(r => r.ingredients)
.Select(i => I.Id));
// use the build in hash set functions to get the ingredients to add / remove
var toAdd = ingrNew.ExpectWith(curIngr);
var toRemove = curIngr.ExpectWith(ingrNew);
foreach (var id in toAdd)
{
// mock the ingredients rather than fetching them, for relations only the id needs to be there
recipe.ingredients.Add(new Ingredient()
{
Id = id
});
}
foreach (var id in toRemove)
{
// again mock only
recipe.ingredients.Remove(new Ingredient()
{
Id = id
});
}
db.SaveChanges();
}
}
If you want it simpler you could just clear all ingredients and re add them if necessary, EF might even be clever enough to figure out that the relations haven't changed, not sure about it though:
public void SetIngredientsForRecipe(long recipeId, List<string> ingredients)
{
using (var db = new FoodEntities(ConnectionString, null, null))
{
var recipe = db.recipe.Single(r => r.ID == recipeId);
// clear all ingredients first
recipe.ingredients.Clear()
var ingrArr = ingredients.ToArray();
var ingrIds = new HasSet<int>(db.ingrediants
.Where(i => ingrArr.Contains(i.Name))
.Select(i => I.Id));
foreach (var id in ingrIds)
{
// mock the ingredients rather than fetching them, for relations only the id needs to be there
recipe.ingredients.Add(new Ingredient()
{
Id = id
});
}
db.SaveChanges();
}
}
UPDATE
Some coding errors have been corrected.

You can condense your Where clauses with the FirstOrDefault calls:
recipe.ingredients.FirstOrDefault(i => i.Name == name);
Though I personally prefer to use SingleOrDefault though I'm not sure what the difference is exactly:
recipe.ingredients.SingleOrDefault(i => i.Name == name);
Also, since the ingredient list that is passed in is a List<string> (as opposed to a list of ingredient IDs), it sort of implies that new ingredients may also be created as part of this process, which isn't handled (though may have been left out for brevity).

Related

How to load another table and use it - using linq to sql

I have a table in my database called Profile, I have another table called ProfileConfirmed. The purpose of this table is to confirm that the person confirmed his email address. I would like to have it that when I loop through all the profiles, the profiles that have not been confirmed do not show up.
Here is my foreach
#foreach (var c in Model.Profiles)
{
}
Here is where I am getting the profiles in my models
public List<Cleaner> GetProfiles()
{
using (var context = new CleanerDataContext(_connectionString))
{
var loadOptions = new DataLoadOptions();
loadOptions.LoadWith<Cleaner>(c => c.ProfileConfirmed);
context.LoadOptions = loadOptions;
return context.Cleaners.ToList();
}
}
I suggest you disregard the 2nd table and add an 'IsConfirmed' with a bit data type column on your 1st table. Then
var query = (from profiles in context.ProfileTable
where IsConfirmed == true select profiles).ToList();
return query;
Less code. Less hassle.
The problem is with this part:
return context.Cleaners.ToList();
ToList() method will always get a new copy of the array, though the objects in it aren't copies, they are the same references as in the original array.
public List<Cleaner> GetProfiles()
{
using (var context = new CleanerDataContext(_connectionString))
{
var loadOptions = new DataLoadOptions();
loadOptions.LoadWith<Cleaner>(c => c.ProfileConfirmed);
context.LoadOptions = loadOptions;
var confirmedProfilers =
from cust in db.SomeTable
where c.ProfileConfirmed == true
select cust;
return confirmedProfilers.ToList();
}
}

how to convert IQueryable<Category> to Category

I have a database with 2 tables: "Albumes" and "Categories".
Each Album has an "id" and a "categoryId" property. although each Category has a "name" property.
I can get the Album.id via QueryString from another page and show the desired Album details in a formview. now i want to show the name of category which contains that album.
I try to do this:
public string GetCategoryName(int albumCategoryId)
{
var _db = new Trying.Models.AlbumContext();
IQueryable<Category> query = _db.Categories;
query = query.Where(c => c.id == albumCategoryId);
Category ca = new Category();
//some code
return ca.name;
}
My problem is in "//some code" section! there, I must convert "IQueryabl query" to "Category ca".
How to do this?
Where returns a set of all matching entities but you only want a single one.
public string GetCategoryName(Int32 albumCategoryId)
{
using (var _db = new Trying.Models.AlbumContext())
{
return _db.Categories.Single(c.id == albumCategoryId).Name;
}
}
Using Single will throw an exception if there is no matching category and you should use this if you are sure that the category has to exist. If you are not sure whether the category exists or not, you can use SingleOrDefault to get null if there is no matching category. First or FirstOrDefault would work, too, but they are semantically wrong because you don't want the first category, you want to only one. SingleOrDefault obviously requires handling the case that there is no matching category.
public string GetCategoryName(Int32 albumCategoryId)
{
using (var _db = new Trying.Models.AlbumContext())
{
var category = _db.Categories.SingleOrDefault(c.id == albumCategoryId);
if (category != null)
{
return category.Name;
}
else
{
// Handle the no category found case by returning a default value,
// throwing an exception or what ever fits your needs.
}
}
}
Also note that you should use your database context in an usingstatement to ensure that it gets early and correctly disposed even in the case of an error.
Further you can access the name of the category directly by navigating through your navigation properties.
var categoryName = album.Category.Name;
This is obviously only useful when you have the album object available and you have to ensure that you load the category by either using lazy loading or explicitly including it if you use eager loading.
var album = _db.Albumes.Include(a => a.Category)
.Single(a => a.Id = 42);
This will load the album with Id 42 and because of Include(a => a.Category) also the category of this album. Whether this is better or worse than explicitly querying the category name depends of course on your requirements.
Try this:
public string GetCategoryName(int albumCategoryId)
{
var _db = new Trying.Models.AlbumContext();
IQueryable<Category> query = _db.Categories;
Category ca = query.First(c => c.id == albumCategoryId);
return ca.name;
}
I will explain with some examples, check the type of the variable because thats what each line is going to return:
list<Category> list = query.Where(c => c.id == albumCategoryId).AsEnumerable();
Category category= query.FisrtOrDefault(c => c.id == albumCategoryId);
IQueryable<T> is a collection.
You need to limit your collection to a single item.
query.Where will also result in a collection. I believe you're looking for a unique item by id. I would recommend Single instead:
var ca = query.Single(c => c.id == albumCategoryId);
return ca.name;

Regarding LINQ Usage in Large Loops

I am wondering what is recommended in the following scenario:
I have a large loop that I traverse to get an ID which I then store in a database like so:
foreach (var rate in rates)
{
// get ID from rate name
Guid Id = dbContext.DifferentEntity
.Where(x => x.Name == rate.Name).FirstOrDefault();
// create new object with the newly discovered
// ID to insert into the database
dbContext.YetAnotherEntity.Add(new YetAnotherEntity
{
Id = Guid.NewGuid(),
DiffId = Id,
}
}
Would it be better/ faster to do this instead (first get all DifferentEntity IDs, rather than querying for them separately)?
List<DifferentEntity> differentEntities = dbContext.DifferentEntity;
foreach (var rate in rates)
{
// get ID from rate name
Guid Id = differentEntities
.Where(x => x.Name == rate.Name).FirstOrDefault();
// create new object with the newly discovered
// ID to insert into the database
dbContext.YetAnotherEntity.Add(new YetAnotherEntity
{
Id = Guid.NewGuid(),
DiffId = Id,
}
}
Is the difference negligible or is this something I should consider? Thanks for your advice.
Store your Rate Names in a sorted string array (string[]) instead of a List or Collection. Then use Array.BinarySearch() to make your search much faster. Rest of what I was going to write has already been written by #Felipe above.
Run them horses! There is really a lot we do not know. Is it possible to keep all the entities in memory? How many of them are duplicates with respect to Name?
A simplistic solution with one fetch from the database and usage of parallelism:
// Fetch entities
var entitiesDict = dbContext.DifferentEntity
.Distinct(EqualityComparerForNameProperty).ToDictionary(e => e.Name);
// Create the new ones real quick and divide into groups of 500
// (cause that horse wins in my environment with complex entities,
// maybe 5 000 or 50 000 fits your scenario better since they are not that complex?)
var newEnts = rates.AsParallel().Select((rate, index) => {
new {
Value = new YetAnotherEntity
{ Id = Guid.NewGuid(), DiffId = entitiesDict[rate.Name],},
Index = index
}
})
.GroupAdjacent(anon => anon.Index / 500) // integer division, and note GroupAdjacent! (not GroupBy)
.Select(group => group.Select(anon => anon.Value)); // do the select so we get the ienumerables
// Now we have to add them to the database
Parallel.ForEach(groupedEnts, ents => {
using (var db = new DBCONTEXT()) // your dbcontext
{
foreach(var ent in ents)
db.YetAnotherEntity.Add(ent);
db.SaveChanges();
}
});
In general in database scenarios, the expensive stuff is the fetch and commits, so try to keep them to a minimum.
You can decrease the number of queries you are doing in database. For example, take all names and query findind Ids where the names contains.
Try something like this.
// get all names you have in rates list...
var rateNames = rates.Select(x => x.Name).ToList();
// query all Ids you need where contains on the namesList... 1 query, 1 column (Id, I imagine)
var Ids = dbContext.DifferentEntity.Where(x => rateNames.Contains(x.Name).Select(x => x.Id).ToList();
// loop in Ids result, and add one by one
foreach(var id in Ids)
dbContext.YetAnotherEntity.Add(new YetAnotherEntity
{
Id = Guid.NewGuid(),
DiffId = id,
}

Entity Framework 4.1. Updating many-to-many relationships. Is this the right way?

The code below works, however, I suspect that I'm missing something. Is there a 'better' way?
private void UpdateNew(MarketProduct marketproduct)
{
context.MarketCategories.Load();
MarketProduct dbProd = context.MarketProducts.Find(marketproduct.Id);
dbProd.Categories.Clear();
foreach (var c in marketproduct.Categories ?? Enumerable.Empty<MarketCategory>())
{
var cc = context.MarketCategories.Find(c.Id);
dbProd.Categories.Add(cc);
}
context.Entry(dbProd).CurrentValues.SetValues(marketproduct);
}
I thought it would be possible to do this without using Find
You have three database queries: 1) context.MarketCategories.Load() (hopefully the category table is small, otherwise this would be a no-go as it loads the whole table into memory), 2) ...Find and 3) dbProd.Categories.Clear(): Here must be a lazy loading involved, otherwise this would crash because dbProd.Categories would be null.
An alternative to update with a single database query is the following:
private void UpdateNew(MarketProduct marketproduct)
{
MarketProduct dbProd = context.MarketProducts
.Include(p => p.Categories)
.Single(p => p.Id == marketproduct.Id);
var categories = marketproduct.Categories
?? Enumerable.Empty<MarketCategory>();
foreach (var category in categories)
{
if (!dbProd.Categories.Any(c => c.Id == category.Id))
{
// means: category is new
context.MarketCategories.Attach(category);
dbProd.Categories.Add(category);
}
}
foreach (var category in dbProd.Categories.ToList())
{
if (!categories.Any(c => c.Id == category.Id))
// means: category has been removed
dbProd.Categories.Remove(category);
}
context.Entry(dbProd).CurrentValues.SetValues(marketproduct);
// context.SaveChanges() somewhere
}
I believe you could just do this:
var dbProd = context.MarketProducts.Find(marketproduct.Id);
dbProd.Categories = dbProd.Categories
.Union(marketproduct.Categories).ToList();
context.SaveChanges();
The Union() call will keep any existing products, add new ones, and update ones that overlap. Since your navigation property Categories is probably defined as an ICollection<Category> you have to use the ToList() extension method during assignment.

How does Entity Framework work with recursive hierarchies? Include() seems not to work with it

I have an Item. Item has a Category.
Category has ID, Name, Parent and Children. Parent and Children are of Category too.
When I do a LINQ to Entities query for a specific Item, it doesn't return the related Category, unless I use the Include("Category") method. But it doesn't bring the full category, with its parent and children. I could do Include("Category.Parent"), but this object is something like a tree, I have a recursive hierarchy and I don't know where it ends.
How can I make EF fully load the Category, with parent and children, and the parent with their parent and children, and so on?
This is not something for the whole application, for performance considerations it would be needed only for this specific entity, the Category.
Instead of using the Include method you could use Load.
You could then do a for each and loop through all the children, loading their children. Then do a for each through their children, and so on.
The number of levels down you go will be hard coded in the number of for each loops you have.
Here is an example of using Load: http://msdn.microsoft.com/en-us/library/bb896249.aspx
If you definitely want the whole hierarchy loaded, then if it was me I'd try writing a stored procedure who's job it is to return all the items in a hierarchy, returning the one you ask for first (and its children subsequently).
And then let the EF's relationship fixup ensure that they are all hooked up.
i.e. something like:
// the GetCategoryAndHierarchyById method is an enum
Category c = ctx.GetCategoryAndHierarchyById(1).ToList().First();
If you've written your stored procedure correctly, materializing all the items in the hierarchy (i.e. ToList()) should make EF relationship fixup kicks in.
And then the item you want (First()) should have all its children loaded and they should have their children loaded etc. All be populated from that one stored procedure call, so no MARS problems either.
Hope this helps
Alex
Use this extension method which calls the hard-coded version of Include, to achieve a dynamic depth level of inclusion, it works great.
namespace System.Data.Entity
{
using Linq;
using Linq.Expressions;
using Text;
public static class QueryableExtensions
{
public static IQueryable<TEntity> Include<TEntity>(this IQueryable<TEntity> source,
int levelIndex, Expression<Func<TEntity, TEntity>> expression)
{
if (levelIndex < 0)
throw new ArgumentOutOfRangeException(nameof(levelIndex));
var member = (MemberExpression)expression.Body;
var property = member.Member.Name;
var sb = new StringBuilder();
for (int i = 0; i < levelIndex; i++)
{
if (i > 0)
sb.Append(Type.Delimiter);
sb.Append(property);
}
return source.Include(sb.ToString());
}
}
}
Usage:
var affiliate = await DbContext.Affiliates
.Include(3, a => a.Referrer)
.SingleOrDefaultAsync(a => a.Id == affiliateId);
Anyway, meanwhile, join the discussion about it on the EF repo.
It could be dangerous if you did happen to load all recursive entities, especially on category, you could end up with WAY more than you bargained for:
Category > Item > OrderLine > Item
OrderHeader > OrderLine > Item
> Item > ...
All of a sudden you've loaded most of your database, you could have also loaded invoices lines, then customers, then all their other invoices.
What you should do is something like the following:
var qryCategories = from q in ctx.Categories
where q.Status == "Open"
select q;
foreach (Category cat in qryCategories) {
if (!cat.Items.IsLoaded)
cat.Items.Load();
// This will only load product groups "once" if need be.
if (!cat.ProductGroupReference.IsLoaded)
cat.ProductGroupReference.Load();
foreach (Item item in cat.Items) {
// product group and items are guaranteed
// to be loaded if you use them here.
}
}
A better solution however is to construct your query to build an anonymous class with the results so you only need to hit your datastore once.
var qryCategories = from q in ctx.Categories
where q.Status == "Open"
select new {
Category = q,
ProductGroup = q.ProductGroup,
Items = q.Items
};
This way you could return a dictionary result if required.
Remember, your contexts should be as short lived as possible.
You don't want to do recursive loading of the hierarchy, unless you are allowing a user to iteratively drill down/up the tree: Every level of recursion is another trip to the database. Similarly, you'll want lazy loading off to prevent further DB trips as you're traversing the hierarchy when rendering to a page or sending over a webservice.
Instead, flip your query: Get Catalog, and Include the items in it. This will get you all items both hierarchically (navigation properties) and flattened, so now you just need to exclude the non-root elements present at the root, which should be pretty trivial.
I had this problem and provided a detailed example of this solution to another, here
You chould rather introduce a mapping table that maps each Category a parent and a child, instead of adding the parent and child property to the cargo itself.
Depending on how often you need that information it can be queried on demand. Via unique constraints in the db you can avoid an infinite amount of relationships beeing possible.
I found out that if you include "two parent levels", you will get the whole parent hierarchy, like that:
var query = Context.Items
.Include(i => i.Category)
.Include(i => i.Category.Parent.Parent)
And now for a completely different approach to hierarchical data, for example populating a treeview.
First, do a flat query for all data, and then build the object graph in memory:
var items = this.DbContext.Items.Where(i=> i.EntityStatusId == entityStatusId).Select(a=> new ItemInfo() {
Id = a.Id,
ParentId = a.ParentId,
Name = a.Name,
ItemTypeId = a.ItemTypeId
}).ToList();
Get the root item:
parent = items.FirstOrDefault(a => a.ItemTypeId == (int)Enums.ItemTypes.Root);
Now build your graph:
this.GetDecendantsFromList(parent, items);
private void GetDecendantsFromList(ItemInfo parent, List<ItemInfo> items)
{
parent.Children = items.Where(a => a.ParentId == parent.Id).ToList();
foreach (var child in parent.Children)
{
this.GetDecendantsFromList(child,items);
}
}
Here is a clever recursive function I found here that would work for this:
public partial class Category
{
public IEnumerable<Category> AllSubcategories()
{
yield return this;
foreach (var directSubcategory in Subcategories)
foreach (var subcategory in directSubcategory.AllSubcategories())
{
yield return subcategory;
}
}
}
You could also create a tablevalued function in the database and add that to your DBContext. Then you can call that from your code.
This example requires that you import EntityFramework.Functions from
nuget.
public class FunctionReturnType
{
public Guid Id { get; set; }
public Guid AnchorId { get; set; } //the zeroPoint for the recursion
// Add other fields as you want (add them to your tablevalued function also).
// I noticed that nextParentId and depth are useful
}
public class _YourDatabaseContextName_ : DbContext
{
[TableValuedFunction("RecursiveQueryFunction", "_YourDatabaseContextName_")]
public IQueryable<FunctionReturnType> RecursiveQueryFunction(
[Parameter(DbType = "boolean")] bool param1 = true
)
{
//Example how to add parameters to your function
//TODO: Ask how to make recursive queries with SQL
var param1 = new ObjectParameter("param1", param1);
return this.ObjectContext().CreateQuery<FunctionReturnType>(
$"RecursiveQueryFunction(#{nameof(param1)})", param1);
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//add both (Function returntype and the actual function) to your modelbuilder.
modelBuilder.ComplexType<FunctionReturnType>();
modelBuilder.AddFunctions(typeof(_YourDatabaseContextName_), false);
base.OnModelCreating(modelBuilder);
}
public IEnumerable<Category> GetParents(Guid id)
{
//this = dbContext
return from hierarchyRow in this.RecursiveQueryFunction(true)
join yourClass from this.Set<YourClassThatHasHierarchy>()
on hierarchyRow.Id equals yourClass.Id
where hierarchyRow.AnchorId == id
select yourClass;
}
}
try this
List<SiteActionMap> list = this.GetQuery<SiteActionMap>()
.Where(m => m.Parent == null && m.Active == true)
.Include(m => m.Action)
.Include(m => m.Parent).ToList();
if (list == null)
return null;
this.GetQuery<SiteActionMap>()
.OrderBy(m => m.SortOrder)
.Where(m => m.Active == true)
.Include(m => m.Action)
.Include(m => m.Parent)
.ToList();
return list;
#parliament gave me an idea for EF6. Example for Category with Methods to load all parents up to root node and all children.
NOTE: Use this only for non performance critical operation. Example with 1000 nodes performance from http://nosalan.blogspot.se/2012/09/hierarchical-data-and-entity-framework-4.html.
Loading 1000 cat. with navigation properties took 15259 ms
Loading 1000 cat. with stored procedure took 169 ms
Code:
public class Category
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public int? ParentId { get; set; }
public virtual Category Parent { get; set; }
public virtual ICollection<Category> Children { get; set; }
private IList<Category> allParentsList = new List<Category>();
public IEnumerable<Category> AllParents()
{
var parent = Parent;
while (!(parent is null))
{
allParentsList.Add(parent);
parent = parent.Parent;
}
return allParentsList;
}
public IEnumerable<Category> AllChildren()
{
yield return this;
foreach (var child in Children)
foreach (var granChild in child.AllChildren())
{
yield return granChild;
}
}
}
My suggestion would be
var query = CreateQuery()
.Where(entity => entity.Id == Id)
.Include(entity => entity.Parent);
var result = await FindAsync(query);
return result.FirstOrDefault();
and it means it will load single entity and all this entity.Parent entities recursive.
entity is same as entity.Parent
public static class EntityFrameworkExtensions
{
public static ObjectContext GetObjectContext(this DbContext context)
{
ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
return objectContext;
}
public static string GetTableName<T>(this ObjectSet<T> objectSet)
where T : class
{
string sql = objectSet.ToTraceString();
Regex regex = new Regex("FROM (?<table>.*) AS");
Match match = regex.Match(sql);
string table = match.Groups["table"].Value;
return table;
}
public static IQueryable<T> RecursiveInclude<T>(this IQueryable<T> query, Expression<Func<T, T>> navigationPropertyExpression, DbContext context)
where T : class
{
var objectContext = context.GetObjectContext();
var entityObjectSet = objectContext.CreateObjectSet<T>();
var entityTableName = entityObjectSet.GetTableName();
var navigationPropertyName = ((MemberExpression)navigationPropertyExpression.Body).Member.Name;
var navigationProperty = entityObjectSet
.EntitySet
.ElementType
.DeclaredNavigationProperties
.Where(w => w.Name.Equals(navigationPropertyName))
.FirstOrDefault();
var association = objectContext.MetadataWorkspace
.GetItems<AssociationType>(DataSpace.SSpace)
.Single(a => a.Name == navigationProperty.RelationshipType.Name);
var pkName = association.ReferentialConstraints[0].FromProperties[0].Name;
var fkName = association.ReferentialConstraints[0].ToProperties[0].Name;
var sqlQuery = #"
EXEC ('
;WITH CTE AS
(
SELECT
[cte1].' + #TABLE_PK + '
, Level = 1
FROM ' + #TABLE_NAME + ' [cte1]
WHERE [cte1].' + #TABLE_FK + ' IS NULL
UNION ALL
SELECT
[cte2].' + #TABLE_PK + '
, Level = CTE.Level + 1
FROM ' + #TABLE_NAME + ' [cte2]
INNER JOIN CTE ON CTE.' + #TABLE_PK + ' = [cte2].' + #TABLE_FK + '
)
SELECT
MAX(CTE.Level)
FROM CTE
')
";
var rawSqlQuery = context.Database.SqlQuery<int>(sqlQuery, new SqlParameter[]
{
new SqlParameter("TABLE_NAME", entityTableName),
new SqlParameter("TABLE_PK", pkName),
new SqlParameter("TABLE_FK", fkName)
});
var includeCount = rawSqlQuery.FirstOrDefault();
var include = string.Empty;
for (var i = 0; i < (includeCount - 1); i++)
{
if (i > 0)
include += ".";
include += navigationPropertyName;
}
return query.Include(include);
}
}
Let me offer my simple solution that fits needs to enable/disable the branch of hierarchical data of the selected department's structure of an organization.
The table Departments looks according this SQL
CREATE TABLE [dbo].[Departments](
[ID] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](1000) NOT NULL,
[OrganizationID] [int] NOT NULL,
[ParentID] [int] NULL,
[IsEnabled] [bit] NOT NULL,
CONSTRAINT [PK_Departments] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
C# code provides a very simple approach that work fine for me.
1. It returns the complete table asynchronously.
2. It changes property for the linked rows.
public async Task<bool> RemoveDepartmentAsync(int orgID, int depID)
{
try
{
using (var db = new GJobEntities())
{
var org = await db.Organizations.FirstOrDefaultAsync(x => x.ID == orgID); // Check if the organization exists
if (org != null)
{
var allDepartments = await db.Departments.ToListAsync(); // get all table items
var isExisting = allDepartments.FirstOrDefault(x => x.OrganizationID == orgID && x.ID == depID);
if (isExisting != null) // Check if the department exists
{
isExisting.IsEnabled = false; // Change the property of visibility of the department
var all = allDepartments.Where(x => x.OrganizationID == orgID && x.ID == isExisting.ID).ToList();
foreach (var item in all)
{
item.IsEnabled = false;
RecursiveRemoveDepartment(orgID, item.ID, ref allDepartments); // Loop over table data set to change property of the linked items
}
await db.SaveChangesAsync();
}
return true;
}
}
}
catch (Exception ex)
{
logger.Error(ex);
}
return false;
}
private void RecursiveRemoveDepartment(int orgID, int? parentID, ref List<Department> items)
{
var all = items.Where(x => x.OrganizationID == orgID && x.ParentID == parentID);
foreach (var item in all)
{
item.IsEnabled = false;
RecursiveRemoveDepartment(orgID, item.ID, ref items);
}
}
This approach works very fast for relative small amount of records I guess less 100000.
Probably for big set of data you have to implement server side stored function.
Enjoy!

Categories

Resources