System.InvalidOperationException while dynamically enumerating and retrieving .NET6 Entity Framework DbContext DbSet<...>s instances' records - reflection

The subject runtime error's location is inside the following code. Please advise how to fix this issue.
using (var ctx = new MyDbContext())
{
foreach (var entityInfo in ctx.GetEntitiesInfo())
System.Console.WriteLine($"{entityInfo.Index}. EntityName = {entityInfo.Name}, Records count = {entityInfo.RecordCount}");
}
public static class Extensions
{
public readonly record struct EntityInfo (int Index, string? Name, int RecordCount);
public static IEnumerable<EntityInfo> GetEntitiesInfo(this MyDbContext context)
{
var dbSetProperties = context.GetDbSetProperties();
var dbSets = dbSetProperties.Select(x => x.GetValue(context, null)).ToList();
var index = 0;
foreach (IQueryable? dbSet in dbSets)
{
++index;
// Runtime Error happens on next code line on second iteration
// when index = 2:
//
// System.InvalidOperationException:
// 'There is already an open DataReader associated with
// this Connection which must be closed first
//-
dbSet.Load();
var items = new List<object>();
var enumerator = dbSet?.GetEnumerator();
while (enumerator?.MoveNext() == true)
{
var item = enumerator.Current;
items.Add(item);
}
yield return new EntityInfo(index, dbSet?.ElementType?.ToString(), items.Count);
}
}
public static IEnumerable<PropertyInfo> GetDbSetProperties(this MyDbContext context)
{
foreach (var property in context.GetType().GetProperties())
if (property?.PropertyType?.FullName?
.Contains("Microsoft.EntityFrameworkCore.DbSet`") == true)
yield return property;
}
}
[Update]
Actually I wanted to develop a generic code to get all DbSet(s) of a given DbContext. Here it's based on Ivan Stoev's answer to this topic:
public static IEnumerable<IQueryable<object>> GetDbSets(this MyDbContext context)
{
var dbSetProperties = context.GetDbSetTypeProperties();
return dbSetProperties.Select(x => x.GetValue(context, null))
.Cast<IQueryable<object>>();
}
public static IEnumerable<PropertyInfo> GetDbSetTypeProperties(this MyDbContext context)
{
foreach (var property in context.GetType().GetProperties())
if (property?.PropertyType?.FullName?
.Contains("Microsoft.EntityFrameworkCore.DbSet`") == true)
yield return property;
}
Please advise if the above code can be even more simplified/streamlined.
[Update 2]
GetDbSets() method can be simplified this way:
public static IEnumerable<IQueryable<object>> GetDbSets(this MyDbContext context)
{
return context
.GetType()
.GetRuntimeProperties()
.Where(x => x.PropertyType?.FullName?.Contains("Microsoft.EntityFrameworkCore.DbSet`") == true)
.Select(x => x.GetValue(context, null))
.Cast<IQueryable<object>>();
}
but is there any other ways to enumerate all DbSet(s) of a given DbContext?

There are a lot of flaws in that code snippet, but the concrete problem in question is caused by the lack of disposal of the IEnumerator returned by the IEnumerable.GetEnumerator() call.
I know the non generic IEnumerator does not implement IDisposable, but that was a miss, and the generic IEnumerator<T> added later normally implements both IEnumerator and IDisposable.
So you have to either account for that and modify the code similar to this:
var items = new List<object>();
var enumerator = dbSet?.GetEnumerator();
try
{
while (enumerator?.MoveNext() == true)
{
var item = enumerator.Current;
items.Add(item);
}
}
finally
{
(enumerator as IDisposable)?.Dispose();
}
or better let the C# foreach do that for you (writing behind the scenes a code similar to the above):
var items = new List<object>();
foreach (var item in dbSet)
items.Add(item);
Note that DbSet<T> type properties are the equivalent of Set<T>() method and are initialized by EF Core at the DbContext instance creation, so neither they nor their content is null, hence all the ?. operators are not needed.
Also calling the Load method is not needed since all it does is to enumerate the db set without storing items in a list (basically foreach with empty body).
Anyway, both previous code snippets will fix the original issue. But there is even better ways. Since all entities are required to be classes (reference types), each db set can be cast to IQueryable<object>
foreach (IQueryable<object> dbSet in dbSets)
which offers some additional benefits.
First, the original code could be fixed just by adding single using statement:
var items = new List<object>();
using var enumerator = dbSet?.GetEnumerator();
while (enumerator?.MoveNext() == true)
{
var item = enumerator.Current;
items.Add(item);
}
It can be replaced with foreach as before
var items = new List<object>();
foreach (var item in dbSet)
items.Add(item);
but now you have access to many Enumerable (and Queryable and EF Core specific) extension methods, so you can replace all the iteration code with simple
var items = dbSet.ToList();
And in case you just need the record count, retrieve it with server side query without loading all the data in memory
var itemsCount = dbSet.Count();
yield return new EntityInfo(index, dbSet.ElementType.ToString(), itemsCount);

Related

Setting parameters when triggering a template deployment via ResourceManagementClient

Is there an easy way of setting parameters in code when setting up a Deployment via ResourceManagementClient in code, without having to go via JObject?
Thanks for the pointer. I've generalised it a bit so the following converts a dictionary into the structure that the deployment request needs:
public static JObject ConvertProperties(this Dictionary<String, Object> properties)
{
if (properties == null || properties.Count == 0)
{
return null;
}
JObject Output = new JObject();
foreach (KeyValuePair<String, Object> TargetProperty in properties)
{
JObject Child = new JObject();
Child["value"] = JToken.FromObject(TargetProperty.Value);
Output[TargetProperty.Key] = Child;
}
return Output;
}

Validate Modified Model Using Annotations in EntityFramwork and ASPNET

I have this class as a part of EF Model:
class Person {
public int Id { get; set; }
[MaxLength(100, ErrorMessage="Name cannot be more than 100 characters")]
public string Name { get; set; }
}
And I have this method in my controller:
public IActionResult ChangeName(int id, string name) {
var person = db.Persons.Find(id);
if(person == null) return NotFound();
person.Name = name;
db.SaveChanges();
return Json(new {result = "Saved Successfully"});
}
Is there any way to validate person after changing the Name property using the annotation MaxLength rather than manually check for it. Becuase sometimes I might have more than one validation and I don't want to examine each one of them. Also, I might change these parameters in the future (e.g. make the max length 200), and that means I have to change it everywhere else.
So is it possible?
Your method works as long as there is one validation error per property. Also, it's quite elaborate. You can use db.GetValidationErrors() to get the same result. One difference is that errors are collected in a collection per property name:
var errors = db.GetValidationErrors()
.SelectMany(devr => devr.ValidationErrors)
.GroupBy(ve => ve.PropertyName)
.ToDictionary(ve => ve.Key, ve => ve.Select(v => v.ErrorMessage));
Okay, I found a solution to my problem, I created a method that takes the model and checks for errors:
private IDictionary<string, string> ValidateModel(Person model)
{
var errors = new Dictionary<string, string>();
foreach (var property in model.GetType().GetProperties())
{
foreach (var attribute in property.GetCustomAttributes())
{
var validationAttribute = attribute as ValidationAttribute;
if(validationAttribute == null) continue;
var value = property.GetValue(model);
if (!validationAttribute.IsValid(value))
{
errors.Add(property.Name, validationAttribute.ErrorMessage);
}
}
}
return errors;
}
UPDATE:
As stated by #Gert Arnold, the method above returns only one validation per property. Below is the fixed version which returns a list of errors for each property
public static IDictionary<string, IList<string>> ValidateModel(Person model)
{
var errors = new Dictionary<string, IList<string>>();
foreach (var property in model.GetType().GetProperties())
{
foreach (var attribute in property.GetCustomAttributes())
{
var validationAttribute = attribute as ValidationAttribute;
if (validationAttribute == null) continue;
var value = property.GetValue(model);
if (validationAttribute.IsValid(value)) continue;
if (!errors.ContainsKey(property.Name))
errors[property.Name] = new List<string>();
errors[property.Name].Add(validationAttribute.ErrorMessage);
}
}
return errors;
}

asp.net web api 2 LINQ to Entities does not recognize the method

I want to invoke method durning mapping my domain class to DTO class but after many tries with LINQ to Entities or LINQ to objects i have failed and i'm getting weird different errors. Actulal error is just a "LINQ to Entities does not recognize the method 'System.String ResizeToLogoImage(System.String)' method, and this method cannot be translated into a store expression.".
Mapping method:
public async Task<SingleCategory> SingleCategoryMapping(EventerApiContext context, int id)
{
var category = await context.Category.Select(c => new SingleCategory
{
CategoryId = c.CategoryId,
CategoryName = c.CategoryName,
CityId = c.CityId,
Events = context.Event.ToList().Where(e=>e.CategoryId == id).Select(e=> new EventForSingleCategory
{
EventId = e.EventId,
EventName = e.EventName,
EventLogo = ImageProcessor.ResizeToLogoImage(e.EventDetail.EventImage.EventImageBase64)
}).ToList()
}).SingleOrDefaultAsync(c => c.CategoryId == id);
return category;
}
Method to be invoked.
public static string ResizeToLogoImage(string base64String)
{
if (base64String == null)
{
return "NULL";
}
var imageToResize = Base64ToImage(base64String);
var resizedImage = ScaleImage(imageToResize, 50, 50);
return ImageToBase64(resizedImage, imageToResize.RawFormat);
}
I know error is appearing during EventLogo property mapping but i have no more idea what to do.
Try to get the data first, before you do the Select statement. I suspect that it is trying to execute ResizeToLogoImage on the database :)

Using one session per request, how to handle updating child objects

I'm having some serious issues with Fluent Nhibernate in my ASP.NET WebForms app when trying to modify a child object and then saving the parent object.
My solution is currently made of 2 projects :
Core : A class library where all entities & repositories classes are located
Website : The ASP.NET 4.5 WebForms application
Here is my simple mapping for my Employee object:
public class EmployeeMap : ClassMap<Employee>
{
public EmployeeMap()
{
Id(x => x.Id).GeneratedBy.Identity();
Map(x => x.DateCreated);
Map(x => x.Username);
Map(x => x.FirstName);
Map(x => x.LastName);
HasMany(x => x.TimeEntries).Inverse().Cascade.All().KeyColumn("Employee_id");
}
}
Here is my my mapping for the TimeEntry object:
public class TimeEntryMap : ClassMap<TimeEntry>
{
public TimeEntryMap()
{
Id(x => x.Id).GeneratedBy.Identity();
Map(x => x.DateCreated);
Map(x => x.Date);
Map(x => x.Length);
References(x => x.Employee).Column("Employee_id").Not.Nullable();
}
}
As stated in the title, i'm using one session per request in my web app, using this code in Gobal.asax:
public static ISessionFactory SessionFactory = Core.SessionFactoryManager.CreateSessionFactory();
public static ISession CurrentSession
{
get { return (ISession)HttpContext.Current.Items["current.session"]; }
set { HttpContext.Current.Items["current.session"] = value; }
}
protected Global()
{
BeginRequest += delegate
{
System.Diagnostics.Debug.WriteLine("New Session");
CurrentSession = SessionFactory.OpenSession();
};
EndRequest += delegate
{
if (CurrentSession != null)
CurrentSession.Dispose();
};
}
Also, here is my SessionFactoryManager class:
public class SessionFactoryManager
{
public static ISession CurrentSession;
public static ISessionFactory CreateSessionFactory()
{
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(c => c.FromConnectionStringWithKey("Website.Properties.Settings.WebSiteConnString")))
.Mappings(m => m
.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly()))
.ExposeConfiguration(cfg => new SchemaUpdate(cfg).Execute(false, true))
.BuildSessionFactory();
}
public static ISession GetSession()
{
return (ISession)HttpContext.Current.Items["current.session"];
}
}
Here is one of my repository class, the one i use to handle the Employee's object data operations:
public class EmployeeRepository<T> : IRepository<T> where T : Employee
{
private readonly ISession _session;
public EmployeeRepository(ISession session)
{
_session = session;
}
public T GetById(int id)
{
T result = null;
using (ITransaction tx = _session.BeginTransaction())
{
result = _session.Get<T>(id);
tx.Commit();
}
return result;
}
public IList<T> GetAll()
{
IList<T> result = null;
using (ITransaction tx = _session.BeginTransaction())
{
result = _session.Query<T>().ToList();
tx.Commit();
}
return result;
}
public bool Save(T item)
{
var result = false;
using (ITransaction tx = _session.BeginTransaction())
{
_session.SaveOrUpdate(item);
tx.Commit();
result = true;
}
return result;
}
public bool Delete(T item)
{
var result = false;
using (ITransaction tx = _session.BeginTransaction())
{
_session.Delete(_session.Load(typeof (T), item.Id));
tx.Commit();
result = true;
}
return result;
}
public int Count()
{
var result = 0;
using (ITransaction tx = _session.BeginTransaction())
{
result = _session.Query<T>().Count();
tx.Commit();
}
return result;
}
}
Now, here is my problem. When i'm trying to insert Employee(s), everything is fine. Updating is also perfect... well, as long as i'm not updating one of the TimeEntry object referenced in the "TimeEntries" property of Employee...
Here is where an exception is raised (in a ASPX file of the web project):
var emp = new Employee(1);
foreach (var timeEntry in emp.TimeEntries)
{
timeEntry.Length += 1;
}
emp.Save();
Here is the exception that is raised:
[NonUniqueObjectException: a different object with the same identifier
value was already associated with the session: 1, of entity:
Core.Entities.Employee]
Basically, whenever I try to
Load an employee and
Modify one of the saved TimeEntry, I get that exception.
FYI, I tried replacing the SaveOrUpdate() in the repository for Merge(). It did an excellent job, but when creating an object using Merge(), my object never gets it's Id set.
I also tried creating and flushing the ISession in each function of my repository. It made no sense because as soon as i was trying to load the TimeEntries property of an Employee, an exception was raised, saying the object could not be lazy-loaded as the ISession was closed...
I'm at lost and would appreciate some help. Any suggestion for my repository is also welcome, as i'm quite new to this.
Thanks you guys!
This code
var emp = new Employee(1);
foreach (var timeEntry in emp.TimeEntries)
{
timeEntry.Length += 1;
}
emp.Save();
is creating a new Employee object, presumable with an ID of 1 passed through the constructor. You should be loading the Employee from the database, and your Employee object should not allow the ID to be set since you are using an identity column. Also, a new Employee would not have any TimeEntries and the error message clearly points to an Employee instance as the problem.
I'm not a fan of transactions inside repositories and I'm really not a fan of generic repositories. Why is your EmployeeRepository a generic? Shouldn't it be
public class EmployeeRepository : IRepository<Employee>
I think your code should look something like:
var repository = new EmployeeRepository(session);
var emp = repository.GetById(1);
foreach (var timeEntry in emp.TimeEntries)
{
timeEntry.Length += 1;
}
repository.Save(emp);
Personally I prefer to work directly with the ISession:
using (var txn = _session.BeginTransaction())
{
var emp = _session.Get<Employee>(1);
foreach (var timeEntry in emp.TimeEntries)
{
timeEntry.Length += 1;
}
txn.Commit();
}
This StackOverflow Answer gives an excellent description of using merge.
But...
I believe that you are facing issues with setting up a correct session pattern for your application.
I you suggest to take a look at session-per-request pattern
where in you create a single NHibernate session object per request. the session is opened when the request is received and closed/flushed on generating a response.
Also make sure that instead of using SessionFactory.OpenSession() to get a session try using SessionFactory.GetCurrentSession() which puts the onus on NHibernate to return you the current correct session.
I hope this pushes you in the right direction.

Caching Results in a Static Variable

I'm using a Linq query to retrieve entities from an SQL server using the Entity Framework. When I update an entitiy, the EF is caching the result. I suspect this is because the ObjectContext is in a static variable (below). The only way to refresh the data using my code below is to call a method and set _db to null when there might be stale data displayed (Eg: in a GridView). Is there a way to just prevent it from caching, or to add some sort of end request handler to call this method on my data layer instead of needing to detect when there may be stale data displayed?
private static ServiceEntities _db;
protected static ServiceEntitiesDb
{
get
{
if (_db == null)
{
_db = new ServiceEntities();
_db.Contacts.MergeOption = MergeOption.OverwriteChanges; // failed
}
return _db;
}
}
public static IEnumerable<Contact> GetContactsByName(string name) {
var items = Db.Contacts;
var filteredName = items.Where(i => (i.Name??string.Empty).IndexOf(name) >=0);
return filteredName;
}
The slightly verbose solution (which I wanted to avoid) is to wrap it in a using block. Ie:
public static IEnumerable<Contact> GetContactsByName(string name) {
var items = Db.Contacts;
var filteredName = items.Where(i => (i.Name??string.Empty).IndexOf(name) >=0);
return filteredName;
}
Becomes
public static IEnumerable<Contact> GetContactsByName(string name) {
using (var db = new SomeContext()) {
var items = db.Contacts;
var filteredName = items.Where(i => (i.Name??string.Empty).IndexOf(name) >=0);
return filteredName;
}
}

Resources