Multi Tenancy and Sharing Contents among tenants

Topics: Administration, Core, Customizing Orchard, Writing modules
Apr 23, 2014 at 3:13 PM
Hello,

I know that the feature in the subject is not currently available in Orchard, so I am trying to come up with a custom module that would address the issue and I would really like some help in understanding a few of the innards of Orchard.

Let's assume (we are prototyping) that I have a part called "Publishable" that I can attach to Content Item. I have created the part with the Handler and Driver and everything works ok. Now when I Save the content item, the Driver method "Editor" will be called on the POST. I have something like this:
        //POST
        protected override DriverResult Editor(PublishablePart part, IUpdateModel updater, dynamic shapeHelper) {
            var item = part.ContentItem;
            var sample = _tenantService.GetTenants();
            updater.TryUpdateModel(part, Prefix, null, null);   

            // TODO: magic happens...

            return Editor(part, shapeHelper);
        }
This was the first idea I came up with and honestly I'm not sure if it's feasible. In the "TODO" in code I would like to Save the contentItem, which I know, to the other tenants... Now, in theory, I know a lot of information, in particular all the DB connection details, so I may as well go into the database and manually add all the parts (although it will require quite a lot of hacking to do since the various parts are unknown).

Another option I was thinking about would be to obtain a ContentManager for that particular tenant (if that is even possible) and fire the "publish" command on it.

Another vague idea would be to actually dive into nHibernate and reset the underlying connection and fire a Save for the second time.

Does any (or a mix) of the above idea sound meaningful? Can anyone provide any insights on how to proceed in this scenario?

Thanks very much!
Leandro.
Developer
Apr 23, 2014 at 10:57 PM

You might be able to shell out to the import export stuff?

Developer
Apr 23, 2014 at 11:20 PM
You're lucky: I just finished a blogpost about how you can do this: Advanced Orchard: accessing other tenants' services :-)
Developer
Apr 23, 2014 at 11:26 PM
Hmmm Is that even safe?


Developer
Apr 23, 2014 at 11:27 PM
What do you mean?
Developer
Apr 23, 2014 at 11:27 PM
Transaction Manager could escalate to MSDTC if you do database access no? if the tenant have different databases.....


Developer
Apr 23, 2014 at 11:32 PM
Could you elaborate how this could cause issues? I don't understand.

This is basically the same code that runs in a background task, wrapping task executions. You probably have to deal with transaction in the work context scope but other otherwise the same happens in bg tasks, so if there is a problem with this there would be one too.
Apr 24, 2014 at 10:26 AM
Edited Apr 24, 2014 at 10:30 AM
Hi Piedone,

Thanks a lot for your reply! It works great... It is a bit like the code Orchard uses in the Specs tests, so when I saw it I immediately realised how to use it :)

This is the code I used:
using (var env = shellContext.LifetimeScope.Resolve<IWorkContextAccessor>().CreateWorkContextScope()) {
    var cm = env.Resolve<IContentManager>();
    var newNews = cm.New("NewsItemModelRecord");
    newNews.As<TitlePart>().Title = "This is good";
    newNews.As<BodyPart>().Text = "<p>And it works!</p>";
    var owner = env.Resolve<ISiteService>().GetSiteSettings().SuperUser;
    var siteOwner = env.Resolve<IMembershipService>().GetUser(owner);
    newNews.As<CommonPart>().Owner = siteOwner;
    cm.Create(item);
}
In the top line I also tried:
using (var env = _orchardHost.CreateStandaloneEnvironment(shellSettings)) {
And it works exactly the same.

Now the last step is to apply this code to the part without knowing what type of ContentItem we are saving... I think the best option would be to hook into the Handler rather than in the Driver, so we know if we are creating or publishing/unpublish.
Developer
Apr 24, 2014 at 12:10 PM
CreateStandaloneEnvironment(), although you can use it in the same way, is something crucially different, see my comments there.
Apr 24, 2014 at 4:27 PM
Good.

Well thanks to both you and Jetski for the help. I ended up with this code in the ContentPartHandler:
protected override void Updated(UpdateContentContext context) {
    var item = context.ContentItem;
    var exported = _services.ContentManager.Export(item);
    if (exported == null) {
        throw new InvalidOperationException("The content item couldn't be multipublished because a handler prevented it from being exported.");
    }
    // TODO get list of tenants from the correct property
    var shellSettings = _tenantService.GetTenants().FirstOrDefault(t => t.Name == "SomeName");
    var shellContext = _orchardHost.GetShellContext(shellSettings);
    using (var env = shellContext.LifetimeScope.Resolve<IWorkContextAccessor>().CreateWorkContextScope()) {
        var cm = env.Resolve<IContentManager>();
        var importer = new ImportContentSession(cm);
        importer.Set(exported.Attribute("Id").Value, exported.Name.LocalName);

        cm.Import(exported, importer);
    }
    base.Updated(context);
}
The only thing I noticed is that the Import was creating a new copy every time... This is because the Import relies on the IdentifierResolverSelector to find the existing item, which does this:
 return _contentManager
     .Query<IdentityPart, IdentityPartRecord>()
     .Where(p => p.Identifier == identifier)
     .List<ContentItem>()
     .Where(c => comparer.Equals(identity, _contentManager.GetItemMetadata(c).Identity));
The subtle effect is that you will only get Published items with this query, so if something is not published it will not be returned and hence recreated (with the same identity!) by the Import process.

I simply registered my own IIdentityResolverSelector with this code:
public class MultiPublishIdentityResolverSelector : IIdentityResolverSelector {
    private readonly IContentManager _contentManager;

    public MultiPublishIdentityResolverSelector(IContentManager contentManager) {
        _contentManager = contentManager;
    }

    public IdentityResolverSelectorResult GetResolver(ContentIdentity contentIdentity) {
        if (contentIdentity.Has("Identifier")) {
            return new IdentityResolverSelectorResult {
                Priority = 10,
                Resolve = MultiPublishResolveIdentity
            };
        }

        return null;
    }

    private IEnumerable<ContentItem> MultiPublishResolveIdentity(ContentIdentity identity) {
        var identifier = identity.Get("Identifier");

        if (identifier == null) {
            return null;
        }

        var comparer = new ContentIdentity.ContentIdentityEqualityComparer();
        return _contentManager
            .Query<IdentityPart, IdentityPartRecord>(VersionOptions.Latest) // <- only difference is here! :)
            .Where(p => p.Identifier == identifier)
            .List<ContentItem>()
            .Where(c => comparer.Equals(identity, _contentManager.GetItemMetadata(c).Identity));
    }
}
And everything seems to work as expected. I'll leave this code here in case someone feel like using it.
Apr 24, 2014 at 5:39 PM
This thread looks like Dol Guldur, the Hill of Sorcery :)