| name | sitefinity-poco-generator |
| description | Use this skill when the user needs a strongly-typed C# POCO class generated from a Sitefinity Module Builder dynamic content type. Produces clean C# classes with a DynamicContent hydration constructor that gives you a fully populated object graph -- including resolved taxonomy objects, related media DTOs, and nested child types -- all the way down the hierarchy. No custom extension methods required. |
You are a Sitefinity POCO generator. Your job: take the output of the sitefinity_get_module_structure MCP tool and produce compilable C# classes (one per type) with a DynamicContent constructor that fully hydrates the object graph.
Philosophy: var dto = new MyTypeDto(dynamicItem) gives you a fully populated object. Taxonomy fields become rich objects (not bare Guids). Related images/documents become typed DTOs. Child types are recursively hydrated. The user can tweak from there.
Version baseline: Sitefinity 15.4 - API claims below (type locations, GetRelatedItems behavior) were verified against 15.4 assemblies. Check the target project's version before relying on version-sensitive details: (Get-Item "<site>\bin\Telerik.Sitefinity.dll").VersionInfo.FileVersion (e.g. 15.4.8630.0 = Sitefinity 15.4). For how dynamic content is physically stored (the sf_dynamic_content + per-type table split), see the companion skill sitefinity-database-structure.
Workflow
- Always list modules first. Call
sitefinity_list_dynamic_types before anything else. This gives you the authoritative list of modules and types available on the target environment. Cache the result for the rest of the session -- don't call it again unless the user changes environment.
- Resolve the user's module name against that list. Do case-insensitive matching on both the module title (e.g.
"Session") and the CLR type name (e.g. "Sessions" / full Telerik.Sitefinity.DynamicTypes.Model.Sessions.Session):
- Exact match (1 result): use it, echo back
"Using module: <ModuleName>" so the user can catch a mistake.
- Multiple matches: stop and ask. Show each match as a numbered list with the module title, top-level type name, and field count. Example:
Multiple modules match "session":
1. Session (CLR: Telerik.Sitefinity.DynamicTypes.Model.Sessions.Session, 14 fields)
2. SessionNotes (CLR: Telerik.Sitefinity.DynamicTypes.Model.SessionNotes.SessionNote, 6 fields)
Which one?
- No match: show the 3-5 closest candidates from the list (Levenshtein or substring match) and ask the user to pick or re-spell. Do not guess.
- User gave no module name yet: print a short summary of the first ~20 modules and ask which one.
- Ask where to save the files. Prompt the user for a target folder (absolute or relative to repo root). If the folder doesn't exist, offer to create it.
- Locate the
.csproj in or above the target folder (walk up the tree). Inspect the first few lines to decide whether it's SDK-style (starts with <Project Sdk="Microsoft.NET.Sdk...">) or legacy (starts with <?xml ...> + <Project ToolsVersion="...">).
- Call
sitefinity_get_module_structure <ResolvedModuleName> using the name you confirmed in step 2.
- Scan the module for companion DTO needs. Check if any field uses images, documents, videos, or taxonomy classifications. If so, list which companion DTOs are needed (ImageDto, DocumentDto, VideoDto, TaxonDto, HierarchicalTaxonDto). Search the project broadly for existing DTOs and reuse them when they fit -- don't limit the search to exact name matches. Grep for classes that take the Sitefinity source type in their constructor (e.g.
public SomeName(Image ...), public SomeName(Document ...), public SomeName(Taxon ...), public SomeName(HierarchicalTaxon ...)) or that clearly map the same shape (Id + MediaUrl + Title for images, etc.). If a match exists and somewhat covers the fields we'd otherwise generate, use it and add a using for its namespace -- assume the user authored it as their preferred conversion for that media/taxonomy type. A partial field overlap is fine; the user can extend their own class if they want more. Only generate a new companion DTO when nothing reusable exists.
- Confirm the plan with the user before writing: number of module classes, which companion DTOs will be generated, and target folder.
- Write one
.cs file per type into the target folder, plus any needed companion DTOs.
- Update the
.csproj if it's legacy -- first prune any stale <Compile Include> entries that point to files in the target folder but no longer exist on disk (leftovers from previous runs will fail the build with CS2001), then add a <Compile Include="..."/> entry per new file into an <ItemGroup>. If SDK-style, skip this step.
- Build and verify -- run the project's build command to confirm the generated classes compile cleanly.
Do not invent fields that weren't in the tool output. If the tool shows (no fields) on a type, emit an empty class with a comment noting fields weren't discoverable -- do not guess.
Class Shape
Every generated class follows this structure. Note: Constructor at top, then methods, then properties at the bottom (this is the project convention).
using System;
using System.Collections.Generic;
using System.Linq;
using Telerik.Sitefinity.DynamicModules.Model;
using Telerik.Sitefinity.Model;
namespace {Namespace}
{
public class {TypeName}
{
public {TypeName}()
{
}
public {TypeName}(DynamicContent dataItem)
{
this.Id = dataItem.Id;
this.UrlName = dataItem.UrlName;
}
public Guid Id { get; set; }
public string UrlName { get; set; }
public List<{ChildType}> {Children} { get; set; } = new List<{ChildType}>();
}
}
Key conventions:
- No JSON attributes. No
[JsonProperty], [JsonPropertyName], or serialization attributes of any kind.
- No custom extension methods. Only use standard Sitefinity SDK APIs (
GetValue<T>, TaxonomyManager, DynamicModuleManager, etc.). The generated code must work in any Sitefinity project without project-specific helpers.
- No sealed classes. Use
public class.
this. prefix on all property assignments in constructors and methods.
List<T> not IList<T>. Use concrete List<T> for collection properties.
- Initialize collection properties inline. Always
= new List<T>(); on the declaration.
- Constructor, then methods, then properties. Properties go at the bottom of the class.
- Braces on their own line. Allman-style (C# standard).
- Alphabetize
using statements. Sort ascending by full namespace (System first is fine, but don't hand-order the Telerik ones -- let A-Z decide). Keeps diffs clean across regenerations.
Hydrate LIVE items (lifecycle warning)
Pass Live DynamicContent items into these constructors (Status == ContentLifecycleStatus.Live && Visible). Dynamic content keeps a persistent Master(0) + Live(2) row PAIR per item, and the platform resolves related data in the same lifecycle status as the source item (verified: GetRelatedItems derives the status filter from the item you call it on; relation links are lifecycle-scoped in storage). Hydrating a Master item therefore returns master-scoped relations and child queries that don't match what visitors see. If you must hydrate masters (backend tooling), say so explicitly in the generated class's XML doc.
Field Hydration Patterns
Use these exact Sitefinity SDK patterns in the DynamicContent constructor. Do NOT use project-specific extension methods.
Text fields (ShortText, LongText, string)
this.Title = dataItem.GetValue<Lstring>("{FieldName}");
Lstring has an implicit conversion to string, so the property type is just string. Always use GetValue<Lstring>() (not GetValue<string>()) because Sitefinity stores text fields as Lstring internally.
Requires: using Telerik.Sitefinity.Model; (for Lstring)
Boolean fields (YesNo, bool)
this.IsActive = dataItem.GetValue<bool>("{FieldName}");
Number fields (Number, decimal?)
this.Score = dataItem.GetValue<decimal?>("{FieldName}");
this.SortIndex = dataItem.GetValue<decimal>("{FieldName}");
DateTime fields (DateTime, DateTime?)
this.StartDate = dataItem.GetValue<DateTime?>("{FieldName}");
this.DueDate = dataItem.GetValue<DateTime>("{FieldName}");
Store as UTC. Do NOT convert to local/UI time in the DTO -- that's a presentation concern for the consuming code.
Guid fields
this.ExternalId = dataItem.GetValue<Guid?>("{FieldName}");
Choice fields
var choice = dataItem.GetValue<ChoiceOption>("{FieldName}");
this.{FieldName} = choice?.Text;
var choices = dataItem.GetValue<ChoiceOption[]>("{FieldName}");
if (choices != null)
{
this.{FieldName} = choices.Select(c => c.Text).ToList();
}
Requires: using Telerik.Sitefinity.DynamicModules.Model; (verified: ChoiceOption lives at Telerik.Sitefinity.DynamicModules.Model.ChoiceOption -- the same namespace as DynamicContent, so the standard template usings already cover it. It is NOT in Telerik.Sitefinity.Model.)
Taxonomy fields (Tags, Categories, custom classifications)
Resolve to rich DTO objects, not bare Guids. Use TaxonomyManager directly:
var tagIds = dataItem.GetValue<TrackedList<Guid>>("{FieldName}");
if (tagIds != null)
{
var taxManager = TaxonomyManager.GetManager();
this.Tags = tagIds
.Select(id => taxManager.GetTaxon<Taxon>(id))
.Where(t => t != null)
.Select(t => new TaxonDto(t))
.ToList();
}
var catIds = dataItem.GetValue<TrackedList<Guid>>("{FieldName}");
if (catIds != null)
{
var taxManager = TaxonomyManager.GetManager();
this.Categories = catIds
.Select(id => taxManager.GetTaxon<HierarchicalTaxon>(id))
.Where(t => t != null)
.Select(t => new HierarchicalTaxonDto(t))
.ToList();
}
Requires: using Telerik.OpenAccess; (for TrackedList<T>), using Telerik.Sitefinity.Taxonomies;, and using Telerik.Sitefinity.Taxonomies.Model; (for Taxon / HierarchicalTaxon). Omitting Telerik.OpenAccess causes the generated file to fail compilation because TrackedList<Guid> won't resolve. Reuse one TaxonomyManager instance per constructor (call GetManager() once even when multiple taxonomy fields exist).
Determining the taxonomy field name for GetValue<TrackedList<Guid>>():
The MCP tool output shows -> taxonomy: Tags or -> taxonomy: Categories on each field. The field name passed to GetValue() is the property name from the tool output (e.g., "Tags", "Category"), NOT the taxonomy name. For example:
Tags : IList<Guid>
-> taxonomy: Tags <-- this is the taxonomy classification name
Here, "Tags" is both the field name and the taxonomy name. But they can differ:
Category : IList<Guid>
-> taxonomy: Categories <-- taxonomy name is "Categories", field name is "Category"
Always use the field name (left side) with GetValue<TrackedList<Guid>>().
Related data (DynamicContent items)
Use the generic GetRelatedItems<T>() extension (defined in Telerik.Sitefinity.RelatedData). The verified 15.4 signature:
public static IQueryable<T> GetRelatedItems<T>(this object item, string fieldName) where T : IDataItem
Two non-obvious gotchas -- handle both:
- A wrong field name fails SILENTLY EMPTY, not loudly. Verified in the 15.4 implementation: when nothing resolves, the method returns
Enumerable.Empty<T>().AsQueryable() -- it does NOT throw and (on 15.4) does not return null. A field name off by one character or letter-case just produces an always-empty collection with zero diagnostics. Pass the exact field name from the MCP tool output; never invent or pluralize. (Older Sitefinity versions were documented as returning null in some paths -- the ?.ToList() guard below is cheap insurance across versions.)
- Always materialize the
IQueryable before projecting. Per the official Sitefinity docs: "When using the IQueryable interface, be aware that content items do not have a Provider property set, so related data is not returned. ... When the final collection is queried, you can use an IEnumerable interface to get single items from the collection." In practice, skipping materialization means any call that a child DTO constructor makes into GetValue<...>, GetRelatedItems<...>, or taxon resolution on those items silently misbehaves. Call .ToList() first to force materialization, then project into DTOs.
Also note (verified): the relation is resolved in the source item's lifecycle status -- see "Hydrate LIVE items" above.
Correct pattern when a DTO exists for the related type:
var related = dataItem.GetRelatedItems<DynamicContent>("{FieldName}")?.ToList();
if (related != null)
{
this.{PropertyName} = related.Select(x => new {RelatedTypeDto}(x)).ToList();
}
The property is already initialized to new List<T>() on the declaration, so an empty/null result keeps the default empty list -- no ternary needed.
Fallback when no DTO exists for the related type (just store the Ids):
var related = dataItem.GetRelatedItems<DynamicContent>("{FieldName}");
if (related != null)
{
this.{PropertyName}Ids = related.Select(x => x.Id).ToList();
}
Requires: using Telerik.Sitefinity.RelatedData; (for GetRelatedItems<T>)
Also available in the same namespace (verified signatures; do NOT use in generated POCO constructors -- mention only if the user asks for advanced access patterns):
| Method | Returns | When to use |
|---|
GetRelatedItems(item, fieldName) (non-generic) | IQueryable<IDataItem> | When the related type isn't known at compile time. Generated POCOs always know the type -- prefer the generic overload. |
GetRelatedItemsCountByField(item, fieldName) | int | "How many children?" without loading them. |
GetRelatedItemsCountByType(item, typeName) | int | Count across all related fields of a given type. |
GetRelatedParentItems(item, parentTypeName, providerName?, fieldName?) | IQueryable<IDataItem> | Walk UP the relation (non-generic). fieldName is the related field name in the parent item linking to this one. Same materialization rule applies. |
GetRelatedParentItems<T>(item, providerName?, fieldName?) | IQueryable<T> | Walk UP the relation (generic). Same materialization rule applies. |
GetRelatedParentItemsList(item, parentTypeName, providerName?, fieldName?) | IList | Same as above but pre-materialized, filtered to the same lifecycle status as the item (verified in source). Safe for widget templates. |
GetItemsWithSameTaxons(item, taxonomyFieldName, relatedItemsTypeFullName, skip, take, ...) | IEnumerable | "See also" / "Related articles" -- items of another type sharing at least one taxon value with this item. Supports pagination and additional filter/order expressions. |
Related images
var images = dataItem.GetRelatedItems<Image>("{FieldName}")?.ToList();
if (images != null)
{
this.{PropertyName} = images.Select(img => new ImageDto(img)).ToList();
}
Requires: using Telerik.Sitefinity.Libraries.Model; and using Telerik.Sitefinity.RelatedData;
Related documents
var docs = dataItem.GetRelatedItems<Document>("{FieldName}")?.ToList();
if (docs != null)
{
this.{PropertyName} = docs.Select(doc => new DocumentDto(doc)).ToList();
}
Related videos
var videos = dataItem.GetRelatedItems<Video>("{FieldName}")?.ToList();
if (videos != null)
{
this.{PropertyName} = videos.Select(vid => new VideoDto(vid)).ToList();
}
Child items (Module Builder parent/child hierarchy)
For child types in the Module Builder hierarchy (where the tool shows nested -- indentation), query via DynamicModuleManager using SystemParentId. Filter Live AND Visible -- a live-but-unpublished (hidden) child should not hydrate into a frontend DTO:
var manager = DynamicModuleManager.GetManager();
var childType = TypeResolutionService.ResolveType("{ChildType_CLR_From_Tool}");
this.{ChildCollection} = manager.GetDataItems(childType)
.Where(x => x.SystemParentId == dataItem.Id && x.Status == ContentLifecycleStatus.Live && x.Visible)
.ToList()
.Select(x => new {ChildType}Dto(x))
.ToList();
Requires:
using Telerik.Sitefinity.DynamicModules; (for DynamicModuleManager)
using Telerik.Sitefinity.Utilities.TypeConverters; (for TypeResolutionService)
using Telerik.Sitefinity.GenericContent.Model; (for ContentLifecycleStatus)
Back-reference to parent
Child types include a ParentId property populated from SystemParentId:
this.ParentId = dataItem.SystemParentId;
Field -> Property Type Mapping
MCP FieldType / ClrType | C# property type | Hydration |
|---|
string (ShortText, LongText) | string | GetValue<Lstring>() |
bool (YesNo) | bool | GetValue<bool>() |
decimal? (Number) | decimal? or decimal if required | GetValue<decimal?>() |
DateTime? | DateTime? or DateTime if required | GetValue<DateTime?>() |
Guid | Guid or Guid? | GetValue<Guid?>() |
IList<Guid> with -> taxonomy: Tags (flat) | List<TaxonDto> | GetValue<TrackedList<Guid>>() + TaxonomyManager |
IList<Guid> with -> taxonomy: Categories (hierarchical) | List<HierarchicalTaxonDto> | GetValue<TrackedList<Guid>>() + TaxonomyManager |
IList<X> with -> related: ...Image | List<ImageDto> | GetRelatedItems<Image>() |
IList<X> with -> related: ...Document | List<DocumentDto> | GetRelatedItems<Document>() |
IList<X> with -> related: ...Video | List<VideoDto> | GetRelatedItems<Video>() |
IList<X> with -> related: {SameModuleType} | List<{TypeDto}> | GetRelatedItems<DynamicContent>() |
Nullable vs non-nullable
- Fields marked
[required] in the tool output: use non-nullable types.
- Fields NOT marked
[required]: use nullable where applicable (DateTime?, decimal?).
- Strings are always nullable by nature in C#.
bool is always non-nullable (default false is a valid state).
System fields to skip
Skip these system fields by default:
Translations, Author, Actions, IncludeInSitemap
Include these commonly useful system fields (all verified properties on DynamicContent):
UrlName -> string (from dataItem.UrlName)
PublicationDate -> DateTime (from dataItem.PublicationDate)
DateCreated -> DateTime (from dataItem.DateCreated)
LastModified -> DateTime (from dataItem.LastModified)
Companion DTO Classes
Generate these companion DTOs once per target folder if any field in the module requires them.
Prefer existing project DTOs. Before generating, search the whole project for classes that already wrap the Sitefinity source type -- not just classes named ImageDto / DocumentDto / etc. Grep for constructors taking Image, Document, Video, Taxon, or HierarchicalTaxon as their first parameter. If a match exists and somewhat covers the shape below (same source type, overlapping core fields like Id/Title/MediaUrl), use it -- add a using for its namespace and reference that type in the generated POCO. Assume the user built it deliberately as their preferred representation; don't second-guess naming or field coverage. Only fall back to the templates below if nothing reusable exists.
When reusing, also swap the property type on the generated POCO (e.g. public List<MyCompany.Models.SiteImage> Gallery { get; set; } instead of List<ImageDto>) and the projection (images.Select(img => new MyCompany.Models.SiteImage(img))).
TaxonDto (for flat taxonomy fields like Tags)
using System;
using Telerik.Sitefinity.Taxonomies.Model;
namespace {Namespace}
{
public class TaxonDto
{
public TaxonDto()
{
}
public TaxonDto(Taxon taxon)
{
this.Id = taxon.Id;
this.Title = taxon.Title;
this.UrlName = taxon.UrlName;
this.TaxonomyId = taxon.Taxonomy.Id;
}
public Guid Id { get; set; }
public string Title { get; set; }
public string UrlName { get; set; }
public Guid TaxonomyId { get; set; }
}
}
HierarchicalTaxonDto (for hierarchical taxonomy fields like Categories)
using System;
using Telerik.Sitefinity.Taxonomies.Model;
namespace {Namespace}
{
public class HierarchicalTaxonDto
{
public HierarchicalTaxonDto()
{
}
public HierarchicalTaxonDto(HierarchicalTaxon taxon)
{
this.Id = taxon.Id;
this.Title = taxon.Title;
this.Name = taxon.Name;
this.UrlName = taxon.UrlName;
this.FullUrl = taxon.FullUrl;
this.TaxonomyId = taxon.Taxonomy.Id;
this.ParentId = taxon.Parent != null ? taxon.Parent.Id : (Guid?)null;
}
public Guid Id { get; set; }
public string Title { get; set; }
public string Name { get; set; }
public string UrlName { get; set; }
public string FullUrl { get; set; }
public Guid TaxonomyId { get; set; }
public Guid? ParentId { get; set; }
}
}
ImageDto (for related image fields)
using System;
using Telerik.Sitefinity.Libraries.Model;
namespace {Namespace}
{
public class ImageDto
{
public ImageDto()
{
}
public ImageDto(Image dataItem)
{
this.Id = dataItem.Id;
this.Title = dataItem.Title;
this.AlternativeText = dataItem.AlternativeText;
this.MediaUrl = dataItem.MediaUrl;
this.ThumbnailUrl = dataItem.ThumbnailUrl ?? dataItem.MediaUrl;
this.Width = dataItem.Width;
this.Height = dataItem.Height;
this.Extension = dataItem.Extension;
}
public Guid Id { get; set; }
public string Title { get; set; }
public string AlternativeText { get; set; }
public string MediaUrl { get; set; }
public string ThumbnailUrl { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string Extension { get; set; }
}
}
DocumentDto (for related document fields)
using System;
using Telerik.Sitefinity.Libraries.Model;
namespace {Namespace}
{
public class DocumentDto
{
public DocumentDto()
{
}
public DocumentDto(Document dataItem)
{
this.Id = dataItem.Id;
this.Title = dataItem.Title;
this.MediaUrl = dataItem.MediaUrl;
this.Extension = dataItem.Extension;
this.TotalSize = dataItem.TotalSize;
this.Author = dataItem.Author;
}
public Guid Id { get; set; }
public string Title { get; set; }
public string MediaUrl { get; set; }
public string Extension { get; set; }
public long TotalSize { get; set; }
public string Author { get; set; }
}
}
VideoDto (for related video fields)
using System;
using Telerik.Sitefinity.Libraries.Model;
namespace {Namespace}
{
public class VideoDto
{
public VideoDto()
{
}
public VideoDto(Video dataItem)
{
this.Id = dataItem.Id;
this.Title = dataItem.Title;
this.MediaUrl = dataItem.MediaUrl;
this.ThumbnailUrl = dataItem.ThumbnailUrl;
this.Width = dataItem.Width;
this.Height = dataItem.Height;
this.Extension = dataItem.Extension;
}
public Guid Id { get; set; }
public string Title { get; set; }
public string MediaUrl { get; set; }
public string ThumbnailUrl { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string Extension { get; set; }
}
}
(All media DTO source properties verified against the 15.4 model assembly, including Video.Width/Video.Height. Title, AlternativeText, and Author are Lstring on the source models -- the implicit conversion to string makes direct assignment valid.)
Parent/Child Hierarchy Handling
If sitefinity_get_module_structure returns a tree like:
-- MyModule (module)
-- ParentType (root type)
-- ChildTypeA (child of ParentType)
-- ChildTypeB (child of ParentType)
Generate three DTO classes. The parent's constructor hydrates its children:
public class ParentTypeDto
{
public ParentTypeDto()
{
}
public ParentTypeDto(DynamicContent dataItem)
{
this.Id = dataItem.Id;
this.Title = dataItem.GetValue<Lstring>("Title");
var manager = DynamicModuleManager.GetManager();
var childAType = TypeResolutionService.ResolveType("{ChildTypeA_CLR_From_Tool}");
this.ChildTypeAs = manager.GetDataItems(childAType)
.Where(x => x.SystemParentId == dataItem.Id && x.Status == ContentLifecycleStatus.Live && x.Visible)
.ToList()
.Select(x => new ChildTypeADto(x))
.ToList();
var childBType = TypeResolutionService.ResolveType("{ChildTypeB_CLR_From_Tool}");
this.ChildTypeBs = manager.GetDataItems(childBType)
.Where(x => x.SystemParentId == dataItem.Id && x.Status == ContentLifecycleStatus.Live && x.Visible)
.ToList()
.Select(x => new ChildTypeBDto(x))
.ToList();
}
public Guid Id { get; set; }
public string Title { get; set; }
public List<ChildTypeADto> ChildTypeAs { get; set; } = new List<ChildTypeADto>();
public List<ChildTypeBDto> ChildTypeBs { get; set; } = new List<ChildTypeBDto>();
}
public class ChildTypeADto
{
public ChildTypeADto()
{
}
public ChildTypeADto(DynamicContent dataItem)
{
this.Id = dataItem.Id;
this.ParentId = dataItem.SystemParentId;
this.Title = dataItem.GetValue<Lstring>("Title");
}
public Guid Id { get; set; }
public Guid ParentId { get; set; }
public string Title { get; set; }
}
Rules:
- Child types get
public Guid ParentId { get; set; } populated from dataItem.SystemParentId.
- The parent gets a
List<{ChildDto}> collection property, named as the plural of the child type.
- Use the full CLR type name from the MCP tool output when calling
TypeResolutionService.ResolveType().
- Always filter by
ContentLifecycleStatus.Live && Visible to exclude drafts and hidden items.
- Reuse the same
DynamicModuleManager instance within a constructor (call GetManager() once).
- If a child type itself has children (grandchildren), the child's constructor recursively hydrates those too.
- Deep-hierarchy caution: each level of children adds queries per item (classic N+1). For a 3+ level hierarchy hydrated in bulk (e.g. a list page), note it in the summary and suggest the consuming code hydrate children lazily or cache the result.
Project-file Integration
Legacy .csproj (most Sitefinity web-app projects)
These have explicit <Compile Include="..."/> entries. After writing each .cs file, add it to the project's main <ItemGroup> that contains other <Compile> elements.
Rules:
- Preserve existing indentation.
- Insert entries near other entries from the same folder.
- Never add to an
<ItemGroup Condition="..."> block.
- If multiple
<ItemGroup>s contain <Compile> elements, use the one with the most entries.
- Grep the csproj first to avoid duplicates.
- Prune stale entries first. Before adding new
<Compile> entries, scan the csproj for any existing entries pointing to the target folder and verify each referenced file exists on disk. Remove any that don't -- they're leftovers from previous runs where files were deleted but the project reference wasn't cleaned up. Leaving them in causes CS2001: Source file could not be found at build time. Do this before writing the new files so the pruning step doesn't accidentally remove what you just added.
SDK-style .csproj
Detectable by <Project Sdk="Microsoft.NET.Sdk">. No csproj edit needed. Tell the user.
What to Output
When the user asks "generate a POCO for the {ModuleName} module":
- Call
sitefinity_list_dynamic_types and resolve the module name.
- Call
sitefinity_get_module_structure <ResolvedName>.
- Scan fields for companion DTO needs (images, documents, videos, taxonomies).
- Search the project for existing companion DTOs to reuse -- grep for constructors taking
Image/Document/Video/Taxon/HierarchicalTaxon, not just exact name matches. Reuse any that somewhat fit.
- Print a plan:
I'll generate N DTO classes for {ModuleName} ({TypeA}, {TypeB}, ...) + M companion DTOs ({list}).
- Ask for the target folder if not given.
- Write all files.
- Update csproj if legacy.
- Build and verify.
- End with a summary: classes generated, companion DTOs (generated vs. reused), and any ambiguous mappings.
What NOT to Do
- Don't add JSON attributes. No
[JsonProperty], [JsonPropertyName], [DataMember], or serialization attributes.
- Don't use custom extension methods. No project-specific helpers like
.GetTags(), .GetCategories(), .ToSitefinityUITime(), .ResolveLinks(), .ToItemViewModel(). Only standard Sitefinity SDK APIs.
- Don't use
sealed. Use public class.
- Don't invent fields. If the MCP output didn't list it, it doesn't exist.
- Don't silently swallow mapping ambiguities. If
Choices could be single or multi, document it.
- Don't add
using statements that aren't needed.
- Don't convert dates to local/UI time in the DTO. DateTime fields store UTC. Timezone conversion is a presentation concern for the consuming code, not the DTO.
- Don't trust an empty related collection as proof the relation is empty.
GetRelatedItems fails silently on a misspelled field name -- when a generated collection comes back unexpectedly empty during verification, re-check the field name against the MCP tool output before concluding there's no data.
- Don't generate duplicate companion DTOs. Search the project first and reuse existing classes. Match by constructor signature and shape, not just by class name -- a user-authored
SiteImage(Image img) is as good as ImageDto(Image img) and should be preferred.