0

I have a class ReportConfigurationManager which manages the CRUD operations against a UserReport entity. The two operations of interest are "Get" and "SaveUpdate". In both cases I wrap the operation in a using statement so that the DbContext is disposed at the end of the query.

Now eventually these methods will form part of a WCF service, but they may also be called internally within the service. My present difficulties are with getting a set of Unit Tests to work which call the ReportConfigurationManager directly.

I can create a new UserReport and save it (this took me a while to solve as the entity has several nested objects which already exist in the database - I needed to "Attach" each of these in turn to the context before calling Add on the UserReport in order to get it to save correctly.

My issues now are with Updates.

Despite having

    context.Configuration.ProxyCreationEnabled = false;
    context.Configuration.AutoDetectChangesEnabled = false;

on ALL methods which use the ReportConfigurationManager, when I came to attach a UserReport, it failed with the classic "an object with the same key already exists in the ObjectStateManager" (I thought disabling Change Tracking was meant to handle this?).

So now I have switched to using the following code which I found here

 public UserReport SaveUpdateUserReport(UserReport userReport)
    {
        using (var context = new ReportDataEF())
        {
            context.Configuration.ProxyCreationEnabled = false;
            context.Configuration.AutoDetectChangesEnabled = false;
            if (userReport.Id > 0)
            {
                {
                    UserReport oldReport = context.UserReports.Where(ur => ur.Id == userReport.Id).FirstOrDefault();
                    context.Entry(oldReport).CurrentValues.SetValues(userReport);
                }                  
            }
            else
            {
                //Need to attach everything to prevent EF trying to create duplicates in the database
                context.ReportTopTypes.Attach(userReport.ReportTopType);
                context.ReportWindows.Attach(userReport.ReportWindow);
                context.ReportSortOptions.Attach(userReport.ReportSortOption);

                foreach (var col in userReport.ReportColumnGroups)
                {
                    context.ReportColumnGroups.Attach(col);
                }

                context.ReportTemplates.Attach(userReport.ReportTemplate);

                //just add the new data
                context.UserReports.Add(userReport);
            }

            context.SaveChanges();
        }

        return userReport;
    }

My concern is that my code seems laborious - I need to get a copy of the old object before I can save the updated copy? And I'm not convinced by my Save New logic either.

So is this approach correct, or is there a better way of writing the above?

Further details of other stuff going on:

Because I'll be sending the object graphs over WCF. I've implemented Eager Loading:

    public static DbQuery<ReportTemplate> IncludeAll(this DbQuery<ReportTemplate> self)
    {
        return self
            .Include("ReportColumnGroups.ReportColumns.ReportColumnType")
            .Include("ReportColumnGroups.ReportColumnType")
            .Include("ReportSortOptions.ReportSortColumns.ReportColumn.ReportColumnType")
            .Include("ReportSortOptions.ReportSortColumns.ReportSortType");
    }

    public static DbQuery<UserReport> IncludeAll(this DbQuery<UserReport> self)
    {
        return self
            .Include("ReportTemplate")
            .Include("ReportTopType")
            .Include("ReportWindow")
            .Include("ReportSortOption.ReportSortColumns.ReportColumn.ReportColumnType")
            .Include("ReportSortOption.ReportSortColumns.ReportSortType")
            .Include("ReportColumnGroups.ReportColumns.ReportColumnType")
            .Include("ReportColumnGroups.ReportColumnType");

    }


    public static DbQuery<ReportSortOption> IncludeAll(this DbQuery<ReportSortOption> self)
    {
        return self
            .Include("ReportSortColumns.ReportColumn.ReportColumnType")
            .Include("ReportSortColumns.ReportSortType");
    }

    public static DbQuery<ReportColumnGroup> IncludeAll(this DbQuery<ReportColumnGroup> self)
    {
        return self
            .Include("ReportColumn.ReportColumnType")
            .Include("ReportColumnType");
    }

    public static DbQuery<ReportColumn> IncludeAll(this DbQuery<ReportColumn> self)
    {
        return self
            .Include("ReportColumnType");
    }

    public static DbQuery<ReportSortColumn> IncludeAll(this DbQuery<ReportSortColumn> self)
    {
        return self
            .Include("ReportColumn.ReportColumnType")
            .Include("ReportSortType");
    }

I have a set of static, cached data that I obtain as follows:

using (var context = new ReportDataEF())
        {
            context.Configuration.ProxyCreationEnabled = false;
            context.Configuration.AutoDetectChangesEnabled = false;
            reportConfigurationData = new ReportingMetaData()
                                          {
                                              WatchTypes = context.WatchTypes.ToList(),
                                              ReportTemplates = context.ReportTemplates.IncludeAll().ToList(),
                                              ReportTopTypes = context.ReportTopTypes.ToList(),
                                              ReportWindows = context.ReportWindows.ToList(),
                                              ReportSortOptions =
                                                  context.ReportSortOptions.IncludeAll().ToList()
                                          };
        }

and I retrieve UserReports as follows:

public UserReport GetUserReport(int userReportId)
    {
        using (var context = new ReportDataEF())
        {
            context.Configuration.ProxyCreationEnabled = false;
            context.Configuration.AutoDetectChangesEnabled = false;
            var visibleReports =
                context.UserReports.IncludeAll().Where(ur => ur.Id == userReportId).FirstOrDefault();
            return visibleReports;
        }
    }

The test I am concerned with gets an existing UserReport from the DB, Updates its ReportTemplate and ReportColumnGroups properties with objects from the static data class and then attempts to save the updated UserReport.

Using the code from Ladislav's answer, this fails when I try to attach the UserReport, presumably because one of the objects I've attached to it, already exists in the database.

Community
  • 1
  • 1
BonyT
  • 10,200
  • 5
  • 28
  • 49

1 Answers1

1

Yes there is another way. First think you should know is that EF doesn't support partially attached object graphs so both Attach and Add have side effects to attach or add all entities in the graph which are not yet tracked by the context. This will simplify your insertion code a lot.

public UserReport SaveUpdateUserReport(UserReport userReport)
{
    using (var context = new ReportDataEF())
    {
        context.Configuration.ProxyCreationEnabled = false;
        context.Configuration.AutoDetectChangesEnabled = false;

        // Now all entities in the graph are attached in unchanged state
        context.ReportTopTypes.Attach(userReport);

        if (userReport.Id > 0 && 
            context.UserReports.Any(ur => ur.Id == userReport.Id))
        {
            context.Entry(userReport).State = EntityState.Modified;
        }
        else
        {
            context.Entry(userReport).State = EntityState.Added;
        }

        context.SaveChanges();
    }

    return userReport;
}

This is equivalent to your original code. You don't load user report again - you just check its existence in DB. This code has a lot of problems - for example if you changed any other related object it will not be persisted to database because currently its state is Unchanged. It can be even more complicated if you need to change relations.

Ladislav Mrnka
  • 349,807
  • 56
  • 643
  • 654
  • In the object graph the only object that is changing is the UserReport itself, but this has a many to many relationship with the ReportColumnGroup class. The set of ReportColumnGroups in the db will never change, but the relationships with UserReports will as reports can have ReportColumnGroups added or removed from the collection. – BonyT Jun 20 '12 at 12:15
  • I thought I'd tried doing what you put, but it failed on the Attach - I've been through many iterations, so will try exactly what you put above and see if it works. – BonyT Jun 20 '12 at 12:16
  • Yes - it fails with the error: An object with the same key already exists in the ObjectStateManager. When i run the line context.UserReports.Attach(userReport); I'll add some more details of my code above. – BonyT Jun 20 '12 at 12:27
  • OK - if I disable LazyLoading, and remove my EagerLoading extensions, then it all seems to work. – BonyT Jun 20 '12 at 13:02
  • Correction - it mostly works - the problem I now have is that none of the many to many relationships are populated. So my UserReport no longer has any ReportColumnGroups retrieved. – BonyT Jun 20 '12 at 13:07
  • OK - So I need to put some Eager loading back in... I think I'll get there eventually :) thanks for your input - immensely grateful – BonyT Jun 20 '12 at 13:18
  • One last question which may explain the issues I had - if there are 2 ways of getting to the same data in the object graph - e.g. A UserReport contains a SortOption and a ColumnGroup, both contain Column objects. Will this cause the error I'm getting on Attach - because it goes down the object hierarchy, adds the columns the first time it comes across them, and then when it goes further into the hierarchy and finds the same data again it throws a fit? – BonyT Jun 20 '12 at 16:52
  • It can cause the issue if the column with the same key is represented by two instances. – Ladislav Mrnka Jun 20 '12 at 18:19