Problem storing custom ContentPartRecord - "No persister found for [name]"

Topics: General
Dec 23, 2014 at 9:22 PM
I'm having troubles storing the ContentPartRecord-part of my contentitem. I'm creating an item as follows:
var product = _productService.GetProduct(productId);
var newModel = _contentManager.New("PricelistModel");
_contentManager.Create(newModel);
var part = newModel.As<PricelistModelPart>();
part.Name = name;
part.Product = product;
_repository.Create(part); //_repository is IRepository<PricelistModelPartRecord>();
I think I have all the plumbing set correctly. Debugging doesn't really seem to be helpful as something isn't working correctly on the mapping side (no idea on where this is done...).

I verified against other ContentPartRecord implementations using Handlers and a IRepository filter and I really can't find a difference. Please help!

Only thing what I can think of is a bug in the mapping where Orchard creates a tablename from a record definition. My tablename is [prefix_Prefix_Module_PricelistModelPartRecord]. The double prefix is correct because the prefix just happens to be the first part of the module name also.

Migration
            SchemaBuilder.CreateTable("PricelistModelPartRecord",
               table => table
                   .ContentPartRecord()
                   .Column<string>("Name")
                   .Column<int>("Product_Id")
           );
Handler
public class PricelistModelPartHandler : ContentHandler
{
    public PricelistModelPartHandler(IOrchardServices orchardServices, IRepository<PricelistModelPartRecord> pricelistModelPartRepository)
    {
        T = NullLocalizer.Instance;

        Filters.Add(StorageFilter.For(pricelistModelPartRepository));
    }
Part definition
public class PricelistModelPart : ContentPart<PricelistModelPartRecord>
    {
        internal LazyField<IContent> _product = new LazyField<IContent>();

        public IContent Product
        {
            get
            {
                return _product.Value;
            }
            set
            {
                _product.Value = value;
            }
        }

        [Required]
        public int ProductId
        {
            get { return Record.ProductId; }
            set { Record.ProductId = value; }
        }

        [Required]
        public string Name
        {
            get { return Record.Name; }
            set { Record.Name = value; }
        }
    }
Record definition
public class PricelistModelPartRecord : ContentPartRecord
    {
        public virtual string Name { get; set; }
        public virtual int ProductId { get; set; }
    }
However I receive a "No persister found for PricelistModelPartRecord" error when I try to create the part (which is attached to the specific contentitem type PricelistModel).
Dec 24, 2014 at 2:23 AM
here's the problem :

ProductId in PricelistModelPartRecord != Product_Id in db.

if you change the migration into :
SchemaBuilder.CreateTable("PricelistModelPartRecord",
           table => table
               .ContentPartRecord()
               .Column<string>("Name")
               .Column<int>("ProductId")
       );
then you should be fine.

but you have another problem, i recommend reading the documentation about lazy field.

thx
juna
Dec 24, 2014 at 11:16 AM
Thanks a lot for looking into my problem. I still get the same error however...

This is what my migration looks like now (i deleted and re-created):
            SchemaBuilder.CreateTable("PricelistModelPartRecord",
               table => table
                   .ContentPartRecord()
                   .Column<string>("Name")
                   .Column<int>("ProductId")
           );
Image

Image

Thanks for pointing out about the LazyField though. I needed that!

Part:
        internal readonly LazyField<IContent> _product = new LazyField<IContent>();
        public LazyField<IContent> ProductField { get { return _product; } }
        public IContent Product { get { return _product.Value; } }
Handler:
            OnActivated<PricelistModelPart>((context, model) =>
            {
                model.ProductField.Value = orchardServices.ContentManager.Get(model.ProductId); //use loader for real lazy functionality
            });
Thanks for pointing out. I now found out about the .Loader function of the LazyField!
Developer
Dec 24, 2014 at 1:21 PM
Edited Dec 24, 2014 at 1:23 PM
Please have a look at the following line in your first code snippet:
_repository.Create(part); //_repository is IRepository<PricelistModelPartRecord>();
This is wrong in two ways:
  1. It looks like you are intending to store a part record, which you shouldn't - the ContentManager will take care of that for you by invoking the appropriate event handlers. To save your new content item, simply invoke IContentManager.Create, passing in your content item.
  2. You are passing in a content part, not a content part record (inferring from your code, the "part" variable is typed "PricelistmodelPart", not "PricelistModelPartRecord". This is the reason you're seeing the persister error - there is no table defined for the "PricelistModelPart" class (you probably intended to insert a "PricelistModelPartRecord" instance, e.g.:
_repository.Create(part.Record); // notice I'm accessing the .Record property of the part.
But again, you shouldn't create part records yourself - the ContentManager takes care of that for you.
All you need to do is this:
var product = _productService.GetProduct(productId);
var newModel = _contentManager.New("PricelistModel");
var part = newModel.As<PricelistModelPart>();
part.Name = name;
part.Product = product;

var versionOptions = VersionOptions.Published; // Or VersionOptions.Draft, whichever makes sense in your case.
_contentManager.Create(newModel, versionOptions);
You could even initialize your new content item inside of a callback, like this:
var product = _productService.GetProduct(productId);
var newModel = _contentManager.New<PricelistModelPart>("PricelistModel");
var versionOptions = VersionOptions.Published; // Or VersionOptions.Draft, whichever makes sense in your case.

_contentManager.Create(newModel, versionOptions, content => {
   content.Name = name;
   content.Product = product;
});
Which is functionally the same, but just a bit more slick I think.
Dec 24, 2014 at 1:29 PM
Edited Dec 24, 2014 at 1:30 PM
I totally get what you are saying about the repository and about content creation. However if you look carefully the error already occurs before I use the repository reference... Also, the contentitem is created, just the partrecord row is nowhere to be found (obviously due to the error).

Edit: I noticed you can't see it properly in the screenshot due to a quickwatch on a further statement. However the error already occurs when I try to access the .Name property...
Developer
Dec 24, 2014 at 1:48 PM
I see. Somehow you're managing to insert a PricelistModelPart into the repository instead of a PricelistModelPartRecord (as per your QuickWatch screenshot).
I take it you did attach the part to the content type (otherwise you would have received a cast error).

Anyway, my best advice at this point is to at least correct your code and maybe try again with a fresh database. If you still get the error, please paste in your complete migration steps, content handler, and the code where you are creating a new content item. We'll find the culprit.
Dec 28, 2014 at 3:38 AM
Sipke's advice is the best way to go. So, here, it's just to add some infos related to drawbacks that I've had before

To use .As<PricelistModelPart>(), even if your part is never displayed, you need to declare a driver, at least an empty one
public class PricelistModelPartDriver : ContentPartDriver<PricelistModelPart> { }
Otherwise, you can cast to ContentPart, but not to your part

If you don't want to create a specific "PricelistModel" content type with your part attached, you can add this filter
Filters.Add(new ActivatingFilter<PricelistModelPart>("PricelistModel"));
If, for a table, you only want to use repository, in migration you don't need the .ContentPartRecord(), but you need
.Column<int>("Id", column => column.PrimaryKey().Identity())
You don't need a Part class, only a Record class not derived from ContentPartRecord, and where you have to map the new "Id" column

I saw that you have first used a Column like "Thing_Id". If it's to have a relation with another table, you don't need to declare a "Thing_Id" in your model, but a "Thing" of type "ThingRecord" (defined for the other table). You need only to assign the Thing property, the "Thing_Id" column will be updated for you

Sorry, maybe too long. So, if you want, we will talk about LazyField another time (take a look on MenuPartHandler.cs: OnActivated<>(), field Setter() and Loader()...)

Hope it can help
Dec 28, 2014 at 1:11 PM
Edited Dec 28, 2014 at 1:13 PM
@jtkech, this is one of the posts i've been waiting for in a long time! This provides and experienced non-Orchard-cms developer with a lot of insights in just a tiny bit of information, thanks!

@sfmskywalker: i corrected code and tried everything in a fresh solution with new database containing just the module. I tried creating the item through the default contents module to rule out any possible faulty code in creating the item. The error is still very clear: No persister for: Com.Pricelist.Models.Parts.PricelistModelPartRecord.
Line 130: }
Line 131: else
Line 132: entityPersister = source.Factory.GetEntityPersister(@event.EntityClassName); Line 133: if (entityPersister == null)
Line 134: throw new HibernateException("Unable to locate persister: " + @event.EntityClassName);
  1. module is called Com.Pricelist
  2. So I have a contentitem "PricelistModel" with a part attached "PricelistModelPart" (via migration, not activating filter).
  3. PricelistModelPartRecord extends ContentPartRecord
  4. PricelistModelPart extends ContentPart<PricelistModelPartRecord>
  5. a table com_Com_Pricelist_PricelistModelPartRecord exists.
  6. Handler, Driver are present
Part:
    public class PricelistModelPart : ContentPart<PricelistModelPartRecord>
    {
        #region Product Lazy Field
        internal readonly LazyField<IContent> _product = new LazyField<IContent>();
        public LazyField<IContent> ProductField { get { return _product; } }
        public IContent Product { get { return _product.Value; } }
        #endregion

        [Required]
        public int ProductId
        {
            get { return Record.ProductId; }
            set { Record.ProductId = value; }
        }

        [Required]
        public string Name
        {
            get { return Record.Name; }
            set { Record.Name = value; }
        }



        [Required]
        public decimal RrpPrice
        {
            get { return Record.RrpPrice; }
            set { Record.RrpPrice = value; }
        }

        [Required]
        public decimal NetPrice
        {
            get { return Record.NetPrice; }
            set { Record.NetPrice = value; }
        }
    }
PartRecord:
    public class PricelistModelPartRecord : ContentPartRecord
    {
        public virtual string Name { get; set; }
        public virtual decimal RrpPrice { get; set; }
        public virtual decimal NetPrice { get; set; }
        public virtual int ProductId { get; set; }
    }
Handler:
    public class PricelistModelPartHandler : ContentHandler
    {
        private readonly IProductManager _productManager;

        public PricelistModelPartHandler(IOrchardServices orchardServices, IRepository<PricelistModelPartRecord> pricelistModelPartRepository)
        {
            T = NullLocalizer.Instance;

            Filters.Add(StorageFilter.For(pricelistModelPartRepository));

            OnActivated<PricelistModelPart>((context, model) =>
            {
                model.ProductField.Value = orchardServices.ContentManager.Get(model.ProductId);
                //could use loader
            });
        }

        public Localizer T { get; set; }

        protected override void GetItemMetadata(GetContentItemMetadataContext context) {
            if (context.ContentItem.ContentType != "PricelistModel")
                return;
            base.GetItemMetadata(context);
        }
    }
Driver:
    public class PricelistModelPartDriver : ContentPartDriver<PricelistModelPart>
    {
        private readonly IContentManager _contentManager;
        private readonly IWorkContextAccessor _workContextAccessor;

        public PricelistModelPartDriver(
            IContentManager contentManager,
            IWorkContextAccessor workContextAccessor)
        {
            _contentManager = contentManager;
            _workContextAccessor = workContextAccessor;
            T = NullLocalizer.Instance;
        }

        public Localizer T { get; set; }

        protected override string Prefix
        {
            get
            {
                return "PricelistModel";
            }
        }

        private readonly IAuthorizationService _authorizationService;
        private readonly IOrchardServices _orchardServices;

        protected override DriverResult Display(PricelistModelPart part, string displayType, dynamic shapeHelper)
        {
            return
                ContentShape("Parts_PricelistModel",
                    () =>
                    {
                        var shape = shapeHelper.Parts_PricelistModel();
                        shape.ContentPart = part;
                        shape.ViewModel = part;
                        return shape;
                    });
        }

        protected override DriverResult Editor(PricelistModelPart part, dynamic shapeHelper)
        {
            return ContentShape("Parts_PricelistModel_Edit", () =>
            {
                return shapeHelper.EditorTemplate(TemplateName: "Parts.PricelistModel.Edit", Model: part, Prefix: Prefix);
            });
        }

        protected override DriverResult Editor(PricelistModelPart part, IUpdateModel updater, dynamic shapeHelper)
        {
            if (updater.TryUpdateModel(part, Prefix, null, null))
            {

            }

            return Editor(part, shapeHelper);
        }
    }
And finally my migration.cs:
        public int Create()
        {
            SchemaBuilder.CreateTable("PricelistModelPartRecord",
               table => table
                   .ContentPartRecord()
                   .Column<string>("Name")
                   .Column<int>("ProductId")
                   .Column<decimal>("RrpPrice")
                   .Column<decimal>("NetPrice")
           );

            ContentDefinitionManager.AlterTypeDefinition(
                "PricelistModel",
                b => b
                .WithPart("PricelistModelPart")
                .Creatable(true).Draftable(false)
                );

            return 1;
        }
Dec 28, 2014 at 1:56 PM
your records should be in Models / Records namespace.
so, instead of "Com.Pricelist.Models.Parts.PricelistModelPartRecord",
change the namespace to "Com.Pricelist.Models.PricelistModelPartRecord".

thx
juna
Dec 30, 2014 at 11:57 AM
Yes! Too bad I didn't get a notification of your reply. I came across a Stackoverflow post which also mentioned the namespacing, but this is indeed the problem. I'm so glad this is sorted out haha.