Cannot import ChannelPart while export works just perfectly

Topics: Core, General, Writing modules
Jun 12, 2012 at 12:36 PM

Hi guys,

I'm pretty new to Orchard and I decided to give it a try by creating a module which I play with different features in.

Abstract: Into a module I have a Channel type which is composed by a CommonPart + ChannelPart -> ChannelPartRecord, created the migration steps, defined the handler and the driver classes, created all CRUD controller actions and appropriate views - everything works just perfectly from the Dashboard and I can manage channels from admin UI.

Problem: Now I want to implement import/export features by overriding appropriate methods(Importing/Exporting) into the ChannelPartDriver. The export works as expected, but I faced a problem with Importing method: The Importing method is not called then I try to import data.

I'm on 1.x branch.

Migrations:

// Channel Part Record
SchemaBuilder.CreateTable(typeof(ChannelPartRecord).Name, table => table
    .ContentPartRecord()
    .Column<int>(Metadata.Fields.CATEGORY_ID)
    .Column<DateTime>(Metadata.Fields.CREATED_AT, x => x.NotNull())
    .Column<string>(Metadata.Fields.DESCRIPTION, x => x.WithLength(Metadata.Defaults.LENGTH_SMALL))
    .Column<bool>(Metadata.Fields.IS_FROZEN, x => x.NotNull())
    .Column<string>(Metadata.Fields.LANGUAGE, x => x.NotNull().WithLength(Metadata.Defaults.LENGTH_TINY))
    .Column<DateTime>(Metadata.Fields.LAST_GRABBED_AT)
    .Column<string>(Metadata.Fields.LAST_GRABBED_STORY, x => x.WithLength(Metadata.Defaults.LENGTH_MEDIUM))
    .Column<string>(Metadata.Fields.NAME, x => x.NotNull().WithLength(Metadata.Defaults.LENGTH_SMALL))
    .Column<int>(Metadata.Fields.SOURCE_ID)
    .Column<string>(Metadata.Fields.URL, x => x.NotNull().WithLength(Metadata.Defaults.LENGTH_MEDIUM))
    .Column<string>(Metadata.Fields.URL_HASH, x => x.NotNull().WithLength(Metadata.Defaults.LENGTH_MEDIUM)));

SchemaBuilder.CreateForeignKey("Channel_Category", typeof(ChannelPartRecord).Name, new[] { Metadata.Fields.CATEGORY_ID }, typeof(CategoryPartRecord).Name, new[] { Metadata.Fields.ID });
SchemaBuilder.CreateForeignKey("Channel_Source", typeof(ChannelPartRecord).Name, new[] { Metadata.Fields.SOURCE_ID }, typeof(SourcePartRecord).Name, new[] { Metadata.Fields.ID });

ContentDefinitionManager.AlterTypeDefinition(Metadata.Types.CHANNEL, x => x
    .WithPart(typeof(CommonPart).Name)
    .WithPart(typeof(ChannelPart).Name));

 

ChannelPartHandler:

public class ChannelPartHandler : ContentHandler {
     #region Constructors and Destructors

     public ChannelPartHandler(IRepository<ChannelPartRecord> repository) {
         Filters.Add(StorageFilter.For(repository));
     }
     #endregion
}

ChannelPartDriver (all overridden methods):

protected override DriverResult Editor(ChannelPart part, dynamic shapeHelper) {
    ChannelViewModel model = BuildModel(part);

    return ContentShape("Parts_ChannelPart_Editor", () => shapeHelper.EditorTemplate(TemplateName: "Parts/ChannelPart.Editor", Model: model, Prefix: Prefix));
}

protected override DriverResult Editor(ChannelPart part, IUpdateModel updater, dynamic shapeHelper) {
    ChannelViewModel model = new ChannelViewModel();
    updater.TryUpdateModel(model, Prefix, null, null);
    if (part.Id != 0) {
        _channelService.Update(part, model);
    }
    return Editor(part, shapeHelper);
}

protected override void Exporting(ChannelPart part, ExportContentContext context) {
    context.Element(part.PartDefinition.Name).SetAttributeValue(Metadata.Fields.CREATED_AT, part.CreatedAt.ToString(CultureInfo.InvariantCulture));
    context.Element(part.PartDefinition.Name).SetAttributeValue(Metadata.Fields.DESCRIPTION, part.Description);
    context.Element(part.PartDefinition.Name).SetAttributeValue(Metadata.Fields.IS_FROZEN, part.IsFrozen.ToString(CultureInfo.InvariantCulture).ToLower());
    context.Element(part.PartDefinition.Name).SetAttributeValue(Metadata.Fields.LANGUAGE, part.Language);

    if (part.LastGrabbedAt.HasValue) {
        context.Element(part.PartDefinition.Name).SetAttributeValue(Metadata.Fields.LAST_GRABBED_AT, part.LastGrabbedAt.Value.ToString(CultureInfo.InvariantCulture));
    }
    context.Element(part.PartDefinition.Name).SetAttributeValue(Metadata.Fields.LAST_GRABBED_STORY, part.LastGrabbedStory);
    context.Element(part.PartDefinition.Name).SetAttributeValue(Metadata.Fields.NAME, part.Name);
    context.Element(part.PartDefinition.Name).SetAttributeValue(Metadata.Fields.URL, part.Url);
}

protected override void Importing(ChannelPart part, ImportContentContext context) {
    // Method is not reached by debugger. Didn't include the code since it's quite long
}

Export.xml

<!--Exported from Orchard-->
<Orchard>
  <Recipe>
    <Name>Generated by Orchard.ImportExport</Name>
    <Author>igornostali</Author>
  </Recipe>
  <Data>
    <Channel Id="" Status="Published">
      <ChannelPart CreatedAt="06/12/2012 10:59:28" IsFrozen="false" Language="ro" Name="Channel #1" Url="http://test.com/rss/news.xml" />
      <CommonPart Owner="/User.UserName=igornostali" CreatedUtc="2012-06-12T07:59:28Z" PublishedUtc="2012-06-12T07:59:28Z" ModifiedUtc="2012-06-12T07:59:28Z" />
    </Channel>
  </Data>
</Orchard>

Exceptions and Logs:
  1. There are no unhandled exceptions.
  2. No error or fatal log entries. However, debug contains the following: Orchard.FileSystems.AppData.AppDataFolder - Could not delete recipe execution folder ..Orchard.Web\App_Data\RecipeQueue\c6c09491cd1f43b2a9c0c3e0709afbfb under "App_Data" folder 

If anyone would like look into I can send the module source code. Just let me know.

Now, my question to you guys is: what am I doing wrong?

Coordinator
Jun 13, 2012 at 5:32 AM

My guess would be that you did not export metadata and on the target system that type doesn't have your part. Or the feature is not enabled, something like that.

Jun 14, 2012 at 10:15 AM

I tried to include metadata into Export.xml and it behaves the same way - Importing is not called and no ChannelPart is imported.

Basically, all metadata should be known since I try to perform export/import operation on the same system. I just:

  1. create a ChannelPart from the UI
  2. go to import-export admin menu item
  3. select Channel type, Metadata and Data to be included into export.xml
  4. after export is completed, I go to Channel list and delete existing item from the UI - so, I have no channels into the system
  5. go to import-export again, and I try to import the file I've just exported
  6. Even if "Your recipe has been imported." is shown, no channels appears into channels list.

Also, I don't think it somehow relates to feature enabled/disabled state since I'm able to export and manage Channels from the UI, thus feature is enabled.

The only guess, after some investigation is that the system somehow "refuse" to import any content parts(again, ANY content parts) which have empty Id into export.xml. I played with other system content parts and if part contains empty id, it is not imported.

Example:

<Channel Id="" Status="Published"> <!-- Custom content part - Won't be imported - Id is empty -->

<Menu Id="" Status="Published"> <!-- System content part - Won't be imported - Id is empty -->
  <CommonPart Owner="/User.UserName=admin" CreatedUtc="2012-06-12T13:37:58Z" PublishedUtc="2012-06-12T13:37:58Z" ModifiedUtc="2012-06-12T13:44:33Z" />
  <TitlePart Title="Main Menu" />
</Menu>

<Page Id="/alias=" Status="Published"> <!--System content part - Will be imported - Id is NOT empty -->

Digging deeper into code I found that whole Import work is handled by DefaultContentManager -> Import opperation, where appropriate content item is retrieved by "Id" from importContentSession (var item = importContentSession.Get(identity);).

...
        // Insert or Update imported data into the content manager.
        // Call content item handlers.
        public void Import(XElement element, ImportContentSession importContentSession) {
            var elementId = element.Attribute("Id");
            if (elementId == null) {
                return;
            }

            var identity = elementId.Value;
            var status = element.Attribute("Status");

            var item = importContentSession.Get(identity);           
...

In my case, when element Id is empty, the content item retrieved from importContentSettion does not have ChannelPart attached(thus item.Parts does not contain ChannelPart) - this is the route cause of my problem because "item" cannot be converted to ChannelPart and driver's Importing method is not called.

ContentPartDriver.cs
...
       void IContentPartDriver.Importing(ImportContentContext context) {
            var part = context.ContentItem.As<TContent>();
            if (part != null) // here I get null, so my ChannelPartDriver's Importing method is not called
                Importing(part, context);
        }
...

Well, let's move to ImportContentSession and find out why Get opperation behaves in such a way. 

        public ContentItem Get(string id) {
            var contentIdentity = new ContentIdentity(id);

            // lookup in local cache
            if (_identities.ContainsKey(contentIdentity))
                return _contentManager.Get(_identities[contentIdentity], VersionOptions.DraftRequired);

            // no result ? then check if there are some more content items to load from the db
            if(_lastIndex != int.MaxValue) {
                
                var equalityComparer = new ContentIdentity.ContentIdentityEqualityComparer();
                IEnumerable<ContentItem> block;
            
                // load identities in blocks
                while ((block = _contentManager.HqlQuery()
                    .ForVersion(VersionOptions.Latest)
                    .OrderBy(x => x.ContentItemVersion(), x => x.Asc("Id"))
                    .Slice(_lastIndex, BulkPage)).Any()) {

                      // Some long logic which retrieves item's metadata from db and adds it to _identities(which is local cache)
                }
            }

            _lastIndex = int.MaxValue;

            if(!_contentTypes.ContainsKey(contentIdentity)) {
                throw new ArgumentException("Unknown content type for " + id);
                
            }

            return _contentManager.New(_contentTypes[contentIdentity]);
        }

Firstly, a try is done to retrieve contentIdentity from local cache. If the contentIdentity is not in cache, database is queried and local cache dictionary is populated with new content type ids. Then the new content item is created and returned.

My assumption regarding this functionality is that: if a try to retrive an item by empty id is done, then an empty contentIdentity is put into cache in pair with first content item which has empty id(it can be any of available content types on the system). And it is always reurned despide of different types I try to import.

Durring my tries, local cache contains different content item on empty identity key, which have no ChannelPart assigned - This is why channels are not imported.

I agree all this explaied stuff might be as difficult to get as it is to explain, so I target Sebastien to help me sort this issue out since he worked on this logic 3 weekes ago.

 

Coordinator
Jun 15, 2012 at 5:33 AM

Right, I had not noticed the empty id. This can't work. If you want a content item to be exportable, it needs to have at least one part that contributes an identity. Autoroute works and there is another identity part that we use on widgets that you can use on your own content types.