Friday, December 19, 2014

Fix “Operation is not valid due to the current state of the object” error during deleting of the field from the list

Some time ago during migrating of the content from old Sharepoint 2007 site to O365 we faced with the following problem: some document libraries in SP 2007 environment had 2 fields with different titles and internal names, but with the same ids. Most probably it was error in one of the custom solution deployed to old site. But because of that problem migration tool could not copy content from such document libraries. Further investigation showed that one of fields with duplicated id is not really used in documents metadata, so it was decided to delete it from doclibs in order to proceed with migration.

I quickly wrote simple utility (remember that source environment was SP2007 where we lived with console utilities instead of PowerShell scripts) which iterates through all sites and lists and checks for duplicated fields. When they are found it deletes field with specified title:

   1: using (var site = new SPSite(url))
   2: {
   3:     using (var web = site.OpenWeb())
   4:     {
   5:         if (!web.Exists)
   6:         {
   7:             return;
   8:         }
   9:  
  10:         // find list by title
  11:         var list = web.Lists.Cast<SPList>().FirstOrDefault(
  12:             l => l.Title == listTitle);
  13:         if (list == null)
  14:         {
  15:             return;
  16:         }
  17:  
  18:         // find field by id and title
  19:         var field = list.Fields.Cast<SPField>().FirstOrDefault(
  20:             f => f.Id == fieldId && f.Title == fieldTitle);
  21:         if (field == null)
  22:         {
  23:             return;
  24:         }
  25:  
  26:         // delete field from list
  27:         list.Fields.Delete(field.InternalName);
  28:         list.Update();
  29:     }
  30: }

It worked properly on test lists created manually, i.e. manually added field was successfully removed by the code above. However on production situation was more complicated: fields were added by content types (although problematic field was not in any bound content types. Probably when developers found the error they removed field from content types, but it stay in provisioned doclibs) and attempt to run the above code there caused exception:

Operation is not valid due to the current state of the object

which was thrown from line 27, i.e. list.Fields.Delete() method. In order to avoid the error I checked implementation of that method in Reflector:

   1: public void Delete(string strName)
   2: {
   3:     this.EnsureFieldsSafeArray(false);
   4:     if (this.ReadOnly)
   5:     {
   6:         throw new SPException(SPResource.GetString(
   7:             "NotAvailOnDetachedFieldCollection", new object[0]));
   8:     }
   9:     SPField fld = this.GetField(strName, false);
  10:     if (fld == null)
  11:     {
  12:         throw new ArgumentException();
  13:     }
  14:     if (!fld.CanBeDeleted)
  15:     {
  16:         throw new InvalidOperationException();
  17:     }
  18:     fld.OnDeleting();
  19:     if (this.m_List == null)
  20:     {
  21:         this.DeleteFromWeb(fld);
  22:     }
  23:     else
  24:     {
  25:         SPSecurity.SetListInHttpContext(HttpContext.Current,
  26:             this.m_List.InternalName);
  27:         this.m_List.Lists.Web.Request.RemoveField(this.m_List.Lists.Web.Url,
  28:             this.m_List.InternalName, fld.InternalName);
  29:     }
  30:     this.SetFieldsStateAsDirty();
  31: }

As shown above InvalidOperationException (exactly for which error message “Operation is not valid …” is displayed for end users) is thrown on line 16 if SPField.CanBeDeleted property of the deleting field is false. Let’s check this property also:

   1: public bool CanBeDeleted
   2: {
   3:     get
   4:     {
   5:         if (this.AllowDeletion.HasValue)
   6:         {
   7:             return this.AllowDeletion.Value;
   8:         }
   9:         return (!this.FromBaseType && !this.Sealed);
  10:     }
  11: }

This is readonly property. However internally it uses another property bool? AllowDeletion which has setter. By default it was null for deleted fields, so in order to avoid the InvalidOperationException I modified the console utility code and added setting of AllowDeletion property to true before to delete the field from list’s fields collection:

   1: using (var site = new SPSite(url))
   2: {
   3:     using (var web = site.OpenWeb())
   4:     {
   5:         if (!web.Exists)
   6:         {
   7:             return;
   8:         }
   9:  
  10:         // find list by title
  11:         var list = web.Lists.Cast<SPList>().FirstOrDefault(
  12:             l => l.Title == listTitle);
  13:         if (list == null)
  14:         {
  15:             return;
  16:         }
  17:  
  18:         // find field by id and title
  19:         var field = list.Fields.Cast<SPField>().FirstOrDefault(
  20:             f => f.Id == fieldId && f.Title == fieldTitle);
  21:         if (field == null)
  22:         {
  23:             return;
  24:         }
  25:  
  26:         // if field can't be deleted, set AllowDeletion to true
  27:         if (!field.CanBeDeleted)
  28:         {
  29:             field.AllowDeletion = true;
  30:             field.Update(true);
  31:         }
  32:  
  33:         // delete field from list
  34:         list.Fields.Delete(field.InternalName);
  35:         list.Update();
  36:     }
  37: }

Additional lines are 26-31. After that code successfully deleted field with duplicated id and it became possible to migrate the content from all document libraries.

No comments:

Post a Comment