IoT Edge Module Scaffolding
Prerequisites
Before scaffolding a module, gather the following information from the user:
- Module name (PascalCase, e.g., "DataProcessorModule")
- Module description (brief description of module purpose)
Scaffolding Process
Follow these steps in order to scaffold a new IoT Edge module:
Step 1: Detect or Load Project Structure
Run the project structure detection script to identify existing patterns:
bash1python scripts/detect_project_structure.py --root .
Step 1.5: Verify IoTEdgeModules Folder Structure
If modules_base_path wasn't found or doesn't exist, ask the user via AskUserQuestion:
- Option 1: Create at default
src/IoTEdgeModules/modules/ - Option 2: Specify custom path
- Option 3: Cancel scaffolding
Processing detection output:
- Parse the JSON output
- If
config_sourceis"saved", use silently - If
config_sourceis"detected", present findings and ask for confirmation via AskUserQuestion:- "Yes, use it" / "Save and use" / "No, customize"
- If "Save and use": run
python scripts/detect_project_structure.py --root . --save - If "No, customize" or detection fails, prompt for: project namespace, container registry URL, modules base path, contracts project name/path
Step 2: Gather Module-Specific Information
Ask the user for:
Required:
- Module name (PascalCase)
- Module description
Optional Features:
A. Private NuGet Feed
- Ask: "Does this project require a private NuGet feed? (Yes/No)"
- If Yes and not detected: Prompt for NuGet feed URL
- If Yes and detected: Confirm detected URL or allow override
B. Shared Contracts Project
- If contracts project was detected: Automatically use it for module constants (no prompt needed)
- If not detected: Ask "Do you have a shared contracts project? (Yes/No/Create standalone)"
Step 3: Validate and Normalize Module Names
Convert the user-provided module name to required formats:
ModuleName (PascalCase):
- Remove "Module" suffix if present, then add it back
- Example: "DataProcessor" → "DataProcessorModule"
- Example: "DataProcessorModule" → "DataProcessorModule"
modulename (lowercase):
- Convert ModuleName to lowercase
- Example: "DataProcessorModule" → "dataprocessormodule"
Confirm with user using AskUserQuestion tool:
Present the module details and ask for confirmation:
- Question: "Proceed with creating this module?"
- Header: "Confirm Module"
- Options:
- "Yes, create module" → Continue to Step 4
- "No, use different name" → Go back to Step 2 (gather module name)
Display in the question description:
Module will be created as:
• C# class name: <ModuleName>
• Module ID: <modulename>
• Directory: <modules_base_path>/<modulename>/
Do NOT assume "Yes" or proceed without using AskUserQuestion tool and getting explicit user confirmation
Step 4: Create Module Directory Structure
Create the module directory:
<modules_base_path>/<modulename>/
Check if directory already exists (MUST use this exact bash syntax):
bash1test -d "<modules_base_path>/<modulename>" && echo "EXISTS" || echo "NOT_EXISTS"
Note: Do NOT use Windows CMD syntax like if exist. Always use Unix bash syntax as shown above.
- If EXISTS: Ask user "Module directory exists. Overwrite? (Yes/Rename/Cancel)"
- If Rename: Prompt for new name and restart from Step 3
Step 5: Generate Module Files from Templates
Use the template files in assets/ to generate module files with runtime substitutions. The skill generates 11 files total.
Placeholder substitutions:
| Placeholder | Value | Example |
|---|---|---|
{{ModuleName}} | PascalCase module name | DataProcessorModule |
{{modulename}} | Lowercase module name | dataprocessormodule |
{{ModuleDescription}} | User-provided description | Processes sensor data |
{{CONTAINER_REGISTRY}} | Detected or provided registry | myregistry.azurecr.io |
{{PROJECT_NAMESPACE}} | Detected or provided namespace | Company.IoT.EdgeAPI |
{{MODULE_CSPROJ_PATH}} | Calculated module csproj path: <modules_base_path>/<modulename>/<ModuleName>.csproj | src/IoTEdgeModules/modules/dataprocessormodule/DataProcessorModule.csproj |
{{MODULE_PUBLISH_PATH}} | Calculated publish path | src/IoTEdgeModules/modules/dataprocessormodule |
{{CONTRACTS_PROJECT_REFERENCE}} | Conditional contracts reference | See below |
{{CONTRACTS_CSPROJ_COPY}} | Conditional Dockerfile COPY | See below |
{{NUGET_CONFIG_SECTION}} | Conditional NuGet configuration | See below |
Conditional placeholder handling:
A. Contracts Project Reference ({{CONTRACTS_PROJECT_REFERENCE}})
Calculate relative path from module directory to contracts directory:
Example:
- Module at:
src/IoTEdgeModules/modules/mydemomodule/ - Contracts at:
src/Company.IoT.Modules.Contracts/ - Relative path:
../../../Company.IoT.Modules.Contracts- Go up 3 levels:
mydemomodule/→modules/→IoTEdgeModules/→src/ - Then down to:
Company.IoT.Modules.Contracts/
- Go up 3 levels:
If using shared contracts:
xml1 <ItemGroup> 2 <ProjectReference Include="<relative-path-to-contracts>/<contracts_project_name>.csproj" /> 3 </ItemGroup>
If NOT using shared contracts:
xml1 <!-- No shared contracts project -->
B. Contracts Dockerfile COPY ({{CONTRACTS_CSPROJ_COPY}})
If using shared contracts:
dockerfile1COPY <contracts_project_path>/*.csproj ./src/
If NOT using shared contracts:
(empty - no COPY line)
C. NuGet Configuration ({{NUGET_CONFIG_SECTION}})
If using private NuGet feed:
dockerfile1ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{\"endpointCredentials\": [{\"endpoint\":\"<nuget_feed_url>\", \"username\":\"docker\", \"password\":\"${FEED_ACCESSTOKEN}\"}]}" 2
If NOT using private NuGet feed:
(empty - no ENV line)
Template file mappings:
| Template File | Target File | Notes |
|---|---|---|
template.csproj | <ModuleName>.csproj | Rename to match ModuleName |
template-module.json | module.json | - |
template-Program.cs | Program.cs | - |
template-Service.cs | <ModuleName>Service.cs | Rename to match ModuleName |
template-GlobalUsings.cs | GlobalUsings.cs | - |
template-ServiceLoggerMessages.cs | <ModuleName>ServiceLoggerMessages.cs | Rename to match ModuleName |
template-Dockerfile.amd64 | Dockerfile.amd64 | - |
template-Dockerfile.amd64.debug | Dockerfile.amd64.debug | - |
template-.dockerignore | .dockerignore | - |
template-.gitignore | .gitignore | - |
template-launchSettings.json | Properties/launchSettings.json | Create Properties/ first |
Processing workflow:
For each template file listed in the table above, process sequentially:
- Read the template file from
assets/ - Replace all placeholders with calculated values
- Write to target location in module directory using the target filename from the table
- Report progress: "✓ Created <filename>"
Process all 11 files one at a time before proceeding to Step 6.
Step 6: Create Shared Contract Constants (Conditional)
If using shared contracts project:
Directory: <contracts_project_path>/<ModuleName>/
File: <ModuleName>Constants.cs
Process:
- Create directory if it doesn't exist
- Read
template-ModuleConstants.cs - Replace placeholders
- Write to contracts project location
If NOT using shared contracts:
Directory: <modules_base_path>/<modulename>/Contracts/
File: <ModuleName>Constants.cs
Process:
- Create
Contracts/folder in module directory - Read
template-ModuleConstants.cs - Replace
{{PROJECT_NAMESPACE}}.Modules.Contractswith just{{ModuleName}}.Contracts - Write to module's Contracts folder
Step 6.5: Create LoggingBuilderExtensions (First Module Only)
This extension method is required for AddModuleConsoleLogging() in Program.cs.
If using shared contracts project:
Check if <contracts_project_path>/Extensions/LoggingBuilderExtensions.cs exists:
- If file exists: Skip this step (already created by previous module)
- If file does NOT exist: Create it
Directory: <contracts_project_path>/Extensions/
File: LoggingBuilderExtensions.cs
Process:
- Create
Extensions/directory if it doesn't exist - Read
template-LoggingBuilderExtensions.cs - Replace
{{PROJECT_NAMESPACE}}placeholder - Write to contracts project Extensions folder
- Report to user: "✓ Created LoggingBuilderExtensions.cs in shared contracts project"
If NOT using shared contracts:
Directory: <modules_base_path>/<modulename>/Extensions/
File: LoggingBuilderExtensions.cs
Process:
- Create
Extensions/folder in module directory - Read
template-LoggingBuilderExtensions.cs - Replace
{{PROJECT_NAMESPACE}}.Modules.Contractswith{{ModuleName}} - Write to module's Extensions folder
Step 7: Scan and Select Deployment Manifests
Run the manifest scanning script:
bash1python scripts/scan_manifests.py --root .
Process the output:
- Parse JSON to get list of manifest files
- If 0 manifests found: Go to Step 7.5 (create base manifest)
- If 1 manifest found: Ask "Add module to <manifest_name>? (Yes/No)"
- If multiple manifests found: Present selection list
Step 7.5: Handle "No Manifests Found" Scenario
If 0 manifests found, ask user to create a base manifest or skip:
- If Yes: Prompt for manifest name (default: "base"), create from
assets/template-base.deployment.manifest.jsonat<manifests_base_path>/{name}.deployment.manifest.json, then add module via the update script - If No: Skip to Step 9
Multi-manifest selection: If multiple manifests found, present list with module counts and let user pick which to update (comma-separated numbers, 'all', or 'none').
Step 8: Update Deployment Manifests (Automated)
For each selected manifest, run the update script:
bash1python scripts/update_deployment_manifest.py \ 2 "<manifest_path>" \ 3 "<modulename>" \ 4 --registry "<container_registry>"
Process the output:
- Check for
"success": truein JSON output - Report to user: "✓ Added <modulename> to <manifest_name> (startup order: <startup_order>)"
- If error: Report error and provide manual fallback instructions
If the script fails, show the error and provide manual fallback instructions from references/deployment-manifests.md.
Step 9: Update README.md (Optional)
Search for "Solution project overview" or "IoTEdge modules" section in README.md:
If section exists:
- Add entry:
- **<modulename>** (\<module_path>`) - <ModuleDescription>` - Insert alphabetically
If section doesn't exist:
- Ask user: "README.md section not found. Create it? (Yes/No)"
Step 9.5: Add Module to Solution File
Detect solution file:
Run the solution detection script:
bash1python scripts/manage_solution.py --root . --detect
Process detection results:
If .slnx file found:
Automatically add module to solution:
bash1python scripts/manage_solution.py \ 2 --root . \ 3 --add-module "<module_csproj_path>" \ 4 --module-name "<ModuleName>"
- Parse JSON output
- If
action: "added": Report "✓ Added to solution at position <insertion_index>" - If
action: "already_exists": Report "Module already in solution" - If
action: "error": Show error and continue
If .sln file found:
- Run manual instruction generator:
bash
1python scripts/manage_solution.py \ 2 --root . \ 3 --add-module "<module_csproj_path>" \ 4 --module-name "<ModuleName>" - Parse JSON output and display
instructionsfield to user - Recommend using:
dotnet sln add "<module_csproj_path>"
If no solution file found:
- Skip this step
- Inform user: "No solution file found. Module created successfully without solution integration."
Step 10: Provide Summary and Next Steps
Summary of created files:
✓ Module scaffolding complete!
Created:
• Module directory: <module_full_path>/ (11 files)
• Constants file: <constants_full_path>
• LoggingBuilderExtensions: <"Created" or "Already exists - skipped">
• Updated manifests: <manifest_count> manifest(s) [or "Created base manifest" if first module]
• Solution integration: <"Added to .slnx" or "Manual instructions provided" or "Skipped">
Configuration:
• Container registry: <container_registry>
• Project namespace: <project_namespace>
• NuGet feed: <nuget_feed_url or "None">
• Shared contracts: <"Yes" or "No">
Next steps for the user:
- Implement business logic in
<ModuleName>Service.cs→ExecuteAsync() - Test locally:
dotnet run --project <module_path>(uses mock IoT Hub client) - Build Docker image, push to registry, deploy manifest to IoT Hub
For post-scaffolding customizations (Quartz, config options, routing, direct methods, etc.), see references/customizations.md.
Reference Documentation
references/module-structure.md- Module structure, naming conventions, and notesreferences/deployment-manifests.md- Deployment manifest configurationreferences/customizations.md- Post-scaffolding customizations, error handling, and config file format