Creating_Package_Providers

about_Creating_Package_Providers

Short Description

Describes how to create a package provider.

Long Description

A package provider is the way for module authors to extend the AnyPackage module. Providers can be created in PowerShell or C#. Package providers are implemented by defining a class that inherits from PackageProvider class and has the PackageProvider attribute.

Supporting List Available

In order to support Get-PackageProvider -ListAvailable the module needs to define shipped providers. In the module manifest have the following key:

@{
    PrivateData = @{
        AnyPackage = @{
            Providers = @('ProviderName')
        }
    }
}

PackageProvider Attribute

The PackageProvider attribute defines the package provider name. Additional configuration may be added in the future to define optional features.

[PackageProvider("ProviderName")]

PackageByName

The PackageByName optional property can be set to $false in order for the provider to indicate that finding/installing/updating packages by package name is not supported. This is useful to set if the provider only supports either by Path or Uri. The default value is $true.

FileExtensions

The FileExtensions optional property is used to indicate which file types are supported by the package provider when finding/installing/updating packages. To use the value a string array is used including the proceeding dot. For example in PowerShell it would be defined like:

[PackageProvider('Msi', FileExtensions = ('.msi', '.msp')]

UriSchemes

The UriSchemes optional property is used to indicate which Uri schemes are supported by the package provider when finding/installing/updating packages. For example in PowerShell it would be defined like:

[PackageProvider('MyProvider', UriSchemes = ('http', 'https')]

PackageProvider Class

The PackageProvider base class serves as the foundation for all package providers.

Constructor

The package provider must have a public parameter-less constructor for AnyPackage to call.

Initializing

AnyPackage creates a new instance of the PackageProvider each time a cmdlet is called. This makes the package provider stateless. If a package provider requires one-time initialization override the Initialize method.

If your provider needs to maintain state between instances then you can create a class that inherits from PackageProviderInfo to store state or user accessible information. The derived PackageProviderInfo will be sent to each instance of the package provider.

[PackageProvider('Test')]
class TestProvider : PackageProvider {
    [PackageProviderInfo] Initialize([PackageProviderInfo] $providerInfo) {
        return [MyProviderInfo]::new()
    }
}

class MyProviderInfo : PackageProviderInfo {
    [SqlConnection] $Connection
}

Uninitializing

To perform one-time provider clean-up to free up any resources or connections override the Clean method.

[PackageProvider('Test')]
class TestProvider : PackageProvider {
    [void] Clean() {
        # Clean-up logic 
    }
}

Supported Sources

If the package provider supports finding/updating/installing/saving packages with a source override the IsSource([string] $source) method. The default implementation is to always return $true.

In this example, the method defines production and testing as supported sources. When an AnyPackage cmdlet is called it will call the IsSource method and validate the provider supports the source.

[bool] IsSource([string] $source) {
    return $source -in 'production', 'testing'
}

Dynamic Parameters

To add provider specific parameters for a command override the GetDynamicParameters method. The $commandName parameter will be one of the cmdlets such as Get-Package. The method can return $null, a [RuntimeDefinedParameterDictionary] object or an object with properties that have [Parameter()] attribute.

Defining a class with properties is the easiest way to create dynamic parameters. The syntax is very similar to the param() block in a PowerShell function. There are few notable differences, one being that each property must have a [Parameter()] attribute in order for the PowerShell runtime to treat it as a parameter. Secondly there is no comma after each parameter.

[PackageProvider('Test')]
class TestProvider : PackageProvider {
    [object] GetDynamicParameters([string] $commandName) {
        if ($commandName -eq 'Get-Package') {
            return [GetPackageDynamicParameters]::new()
        } else {
            return $null
        }
    }
}

class GetPackageDynamicParameters {
    [Parameter()]
    [string] $Path

    [Parameter()]
    [ScopeType] $Scope
}

Supporting Operations

The package provider indicates support for each individual AnyPackage cmdlet by adding a corresponding interface. For example, if the package provider supports Get-Package cmdlet then the IGetPackage interface would be implemented.

Cmdlet Interface
Find-Package IFindPackage
Get-Package IGetPackage
Install-Package IInstallPackage
Publish-Package IPublishPackage
Save-Package ISavePackage
Update-Package IUpdatePackage
Uninstall-Package IUninstallPackage
Get-PackageSource IGetSource
Set-PackageSource ISetSource
Register-PackageSource ISetSource
Unregister-PackageSource ISetSource

If a Package method was successful WritePackage must be called with the package details. In the event a package is not found by the provider do not throw an exception as AnyPackage will write an error.

The package interfaces follow the structure as follows.

[PackageProvider('Test')]
class TestProvider : PackageProvider, IGetPackage {
    [void] GetPackage([PackageRequest] $request) { }
}

If a Source method was successful WriteSource must be called with the source details. In the event a source is not found by the provider do not throw an exception as AnyPackage will write an error.

The package source interfaces follow the structure as follows.

[PackageProvider('Test')]
class TestProvider : PackageProvider, IGetSource {
    [void] GetPackageSource([SourceRequest] $request) { }
}

The ISetSource interface is different as it requires three methods compared to the rest.

[PackageProvider('Test')]
class TestProvider : PackageProvider, ISetSource {
    [void] SetPackageSource([SourceRequest] $request) { }

    [void] RegisterPackageSource([SourceRequest] $request) { }

    [void] UnregisterPackageSource([SourceRequest] $request) { }
}

Package Request

The [PackageRequest] type contains information about the request and methods to interact with AnyPackage.

class PackageRequest {
    [string] $Name
    [PackageVersionRange] $Version
    [string] $Source
    [bool] $Prerelease
    [PackageInfo] $Package
    [string] $Path
    [Uri] $Uri
    [object] $DynamicParameters
    [PackageProviderInfo] $ProviderInfo

    [bool] $Stopping

    [bool] IsMatch([string] $name)
    [bool] IsMatch([PackageVersion] $version)
    [bool] IsMatch([string] $name, [PackageVersion] $version)

    [bool] PromptUntrustedSource([string] $source)

    [void] WritePackage([PackageInfo] $package)

Source Request

The [SourceRequest] type contains information about the request and methods to interact with AnyPackage.

class SourceRequest {
    [string] $Name
    [string] $Location
    [bool] $Trusted
    [bool] $Force
    [object] $DynamicParameters
    [PackageProviderInfo] $ProviderInfo

    [bool] $Stopping

    [void] WriteSource([PackageSourceInfo] $source)

Register a Package Provider

To register a package provider with AnyPackage the following method must be called from within your module. In this example the [TestProvider] is the type that implements the package provider and e5491948-72b3-4f00-aa64-f93060d9b242 is the provider’s unique ID..

[guid] $id = 'e5491948-72b3-4f00-aa64-f93060d9b242'
[PackageProviderManager]::RegisterProvider($id, [TestProvider], $MyInvocation.MyCommand.ScriptBlock.Module)

If you are unable to pass the PSModuleInfo then the module name can be passed instead.

[guid] $id = 'e5491948-72b3-4f00-aa64-f93060d9b242'
[PackageProviderManager]::RegisterProvider($id, [TestProvider], 'TestModule')

Unregister a Package Provider

To remove a package provider on when the provider module is removed set the OnRemove property for the module calling the [PackageProviderManager]::UnregisterProvider([Guid] $id) method.

In the example the following example, the e5491948-72b3-4f00-aa64-f93060d9b242 is the provider’s unique ID.

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { 
    [PackageProviderManager]::UnregisterProvider('e5491948-72b3-4f00-aa64-f93060d9b242')
}

Packages without a Version

If your package provider supports packages without a version special consideration needs to take place. The user may specify all versions to be returned and in that case packages with a null version should also be returned.

In this example the $request.IsMatch($name) method is used to filter package names. Then an additional check is used if the $request.Version is null or is a * all version wildcard.

if ($request.IsMatch($name) -and ($null -eq $request.Version -or $request.Version -eq '*') {
    # Write package
}

Examples

Basic Provider

The following example is the minimum code required to define a package provider.

using module AnyPackage
using namespace AnyPackage.Provider

[PackageProvider('Test')]
class TestProvider : PackageProvider { }

[guid] $id = 'e5491948-72b3-4f00-aa64-f93060d9b242'
[PackageProviderManager]::RegisterProvider($id, [TestProvider], $MyInvocation.MyCommand.ScriptBlock.Module)

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { 
    [PackageProviderManager]::UnregisterProvider($id)
}

Documenting

The package provider should come with an about topic to describe the provider any capabilities it has and any dynamic parameters.

See Also