When you are calling .ToList()
you are pulling back all of those entities into memory for a start. And then by looping through these you are effectively calling that same piece of SQL 45000 times if I understand your situation.
You shouldn't need the entire object returned to delete it. I suggest only pulling out the IDs of the contacts to delete by just selecting the ID in your query:
var ids = db.Groups.Find(id).Contacts.Select(x => x.ContactId).ToList();
I would then look into implementing either EntityFrameworkExtensions library(https://github.com/loresoft/EntityFramework.Extended) to help handle this (making sure to batch up your deletes), or using a IN query, like DELETE FROM Contacts WHERE ContactID IN (1,2,3,....)
but again, making sure to batch this.
The Extensions library lets you do a delete like:
m_context.Contacts.Delete(x => idList.Contains(x.ContactId));
Note however that this will execute immediately, and not when you call SaveChanges()
Overall, the code would be something like this:
public int RemoveContacts(IList<int> _ids)
{
int index = 0;
int numDeleted = 0;
while (index < _ids.Count())
{
var batch= _ids.Skip(index).Take(MAX_BATCH_SIZE);
//Using extensions method
numDeleted += context.Contacts.Delete(x => batch.Contains(x.ContactId));
//Using SQL
context.Database.ExecuteSqlCommand("DELETE FROM Contacts WHERE ContactID IN {0}", batch);
index += MAX_BATCH_SIZE;
}
return numDeleted ;
}