PartCreationPolicy is declared on a type that is not exported

ID

vbnet.correctness.partcreationpolicy_needs_export

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Api Design

Language

VB.NET

Description

Reports a class whose declaration carries <PartCreationPolicy> but no export attribute on the same declaration. <Export>, <ExportAttribute>, <InheritedExport> and <InheritedExportAttribute> all count as an export, in either order relative to the policy. <ExportMetadata> does not: it decorates an export that has to exist independently, so a type that declares metadata and a creation policy but never exports anything is still reported.

Rationale

A creation policy is a property of a composition part, and a type becomes a part by being exported. Without an export on the type there is nothing for the policy to describe, so the container never reads it and the declaration has no effect of any kind - it is not a misconfiguration that changes behaviour, it is metadata the framework never asks for. What makes this worth fixing is the impression the surviving attribute leaves. Shared or NonShared next to a class name is how a reader determines instance lifetime, so the type appears to be a registered singleton or a per-import instance when in fact nothing composes it at all. That mismatch is a symptom rather than the disease: an attribute pair usually arrives together, and finding the policy alone means the export was lost - deleted during a refactor, moved when discovery was switched to a convention-based registration, or never written on a class that was copied from a sibling plug-in. The consequence is then the quiet kind: the import is never satisfied, the feature is absent, and no error identifies the type responsible.

The following code illustrates the pattern detected by this rule:

Namespace Acme.Plugins

    ' FLAGGED: PartCreationPolicy is declared on a type that is not exported
    <PartCreationPolicy(CreationPolicy.Shared)>
    Public Class CsvReportRenderer
        Implements IReportRenderer

        Public Function Render(rows As Integer) As String Implements IReportRenderer.Render
            Return rows.ToString()
        End Function

Remediation

Decide which of the two attributes states the truth. If the class is meant to be a part, add the <Export> that was intended, naming the contract that consumers import - the policy then applies as written. If it is not a part, remove <PartCreationPolicy> so the declaration stops implying a lifetime the container does not manage. When the class is composed through a registration made elsewhere rather than through attributes, express its lifetime there and drop the attribute. The one case to keep is a type whose exports are declared on its properties or methods instead of on the class: the type is still a part, so the policy applies as written.

' Before: the policy is inert, nothing composes this type
<PartCreationPolicy(CreationPolicy.Shared)>
Public Class CsvReportRenderer
    Implements IReportRenderer

' After
<Export(GetType(IReportRenderer))>
<PartCreationPolicy(CreationPolicy.Shared)>
Public Class CsvReportRenderer
    Implements IReportRenderer

Configuration

This detector does not need any configuration.