Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:58:35 +08:00
commit 2448fbf2fb
25 changed files with 2940 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
# Ignore .NET build outputs
**/bin
**/obj

View File

@@ -0,0 +1,34 @@
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
.vs
[Bb]in/
[Oo]bj/

View File

@@ -0,0 +1,33 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim AS build-env
ARG FEED_ACCESSTOKEN
WORKDIR /app
RUN curl -L https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh | sh
RUN mkdir src
COPY {{MODULE_CSPROJ_PATH}}/*.csproj ./src/
{{CONTRACTS_CSPROJ_COPY}}
COPY Directory.Build.props .
COPY .editorconfig .
COPY nuget.config .
{{NUGET_CONFIG_SECTION}}
RUN dotnet restore "./src/{{ModuleName}}.csproj"
COPY src/ ./src/
RUN dotnet publish "{{MODULE_PUBLISH_PATH}}/{{ModuleName}}.csproj" -c Release -o out
FROM mcr.microsoft.com/dotnet/runtime:9.0-bookworm-slim
WORKDIR /app
COPY --from=build-env /app/out ./
ENV USER=moduleuser
ENV PUID=2000
ENV TPM_GID=3000
RUN useradd --uid $PUID --shell /bin/bash --create-home "$USER"
RUN groupadd -f -g $TPM_GID aziottpm
RUN usermod -a -G aziottpm $USER
USER $USER
ENTRYPOINT ["./{{ModuleName}}"]

View File

@@ -0,0 +1,38 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim AS build-env
ARG FEED_ACCESSTOKEN
WORKDIR /app
RUN curl -L https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh | sh
RUN mkdir src
COPY {{MODULE_CSPROJ_PATH}}/*.csproj ./src/
{{CONTRACTS_CSPROJ_COPY}}
COPY Directory.Build.props .
COPY .editorconfig .
COPY nuget.config .
{{NUGET_CONFIG_SECTION}}
RUN dotnet restore "./src/{{ModuleName}}.csproj"
COPY src/ ./src/
RUN dotnet publish "{{MODULE_PUBLISH_PATH}}/{{ModuleName}}.csproj" -c Debug -o out
FROM mcr.microsoft.com/dotnet/runtime:9.0-bookworm-slim
WORKDIR /app
COPY --from=build-env /app/out ./
RUN apt-get update && \
apt-get install -y --no-install-recommends unzip procps && \
rm -rf /var/lib/apt/lists/* && \
curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/vsdbg
ENV USER=moduleuser
ENV PUID=2000
ENV TPM_GID=3000
RUN useradd --uid $PUID --shell /bin/bash --create-home "$USER"
RUN groupadd -f -g $TPM_GID aziottpm
RUN usermod -a -G aziottpm $USER
USER $USER
ENTRYPOINT ["./{{ModuleName}}"]

View File

@@ -0,0 +1,10 @@
global using Atc.Azure.IoTEdge.Extensions;
global using Atc.Azure.IoTEdge.Factories;
global using Atc.Azure.IoTEdge.TestMocks;
global using Atc.Azure.IoTEdge.Wrappers;
global using {{PROJECT_NAMESPACE}}.Modules.Contracts.Extensions;
global using {{PROJECT_NAMESPACE}}.Modules.Contracts.{{ModuleName}};
global using Microsoft.Azure.Devices.Client;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;

View File

@@ -0,0 +1,22 @@
namespace {{PROJECT_NAMESPACE}}.Modules.Contracts.Extensions;
public static class LoggingBuilderExtensions
{
/// <summary>
/// Adds systemd console logging with standardized timestamp format and log level configuration.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to configure.</param>
/// <returns>The <see cref="ILoggingBuilder"/> for chaining.</returns>
public static ILoggingBuilder AddModuleConsoleLogging(this ILoggingBuilder builder)
{
builder.AddSystemdConsole(options =>
{
options.UseUtcTimestamp = true;
options.TimestampFormat = " yyyy-MM-dd HH:mm:ss.fff zzz ";
});
builder.SetMinimumLevel(LogLevel.Information);
return builder;
}
}

View File

@@ -0,0 +1,9 @@
namespace {{PROJECT_NAMESPACE}}.Modules.Contracts.{{ModuleName}};
public static class {{ModuleName}}Constants
{
public const string ModuleId = "{{modulename}}";
//// TODO: Add direct method name constants here
//// public const string DirectMethodExampleMethod = "ExampleMethod";
}

View File

@@ -0,0 +1,35 @@
namespace {{ModuleName}};
public static class Program
{
public static async Task Main(string[] args)
{
using var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddLogging(builder =>
{
builder.AddModuleConsoleLogging();
});
if (hostContext.IsStandaloneMode())
{
services.AddSingleton<IModuleClientWrapper, MockModuleClientWrapper>();
}
else
{
services.AddModuleClientWrapper(TransportSettingsFactory.BuildMqttTransportSettings());
}
services.AddSingleton<IMethodResponseFactory, MethodResponseFactory>();
//// TODO: Add your service registrations here
services.AddHostedService<{{ModuleName}}Service>();
})
.UseConsoleLifetime()
.Build();
await host.RunAsync();
}
}

View File

@@ -0,0 +1,59 @@
namespace {{ModuleName}};
/// <summary>
/// The main {{ModuleName}}Service.
/// </summary>
public sealed partial class {{ModuleName}}Service : IHostedService
{
private readonly IHostApplicationLifetime hostApplication;
private readonly IModuleClientWrapper moduleClient;
public {{ModuleName}}Service(
ILogger<{{ModuleName}}Service> logger,
IHostApplicationLifetime hostApplication,
IModuleClientWrapper moduleClient)
{
this.logger = logger;
this.hostApplication = hostApplication;
this.moduleClient = moduleClient;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
hostApplication.ApplicationStarted.Register(OnStarted);
hostApplication.ApplicationStopping.Register(OnStopping);
hostApplication.ApplicationStopped.Register(OnStopped);
moduleClient.SetConnectionStatusChangesHandler(LogConnectionStatusChange);
await moduleClient.OpenAsync(cancellationToken);
// TODO: Register direct method handlers here
//// await moduleClient.SetMethodHandlerAsync("MethodName", HandleMethodAsync, string.Empty, cancellationToken);
LogModuleClientStarted({{ModuleName}}Constants.ModuleId);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
try
{
await moduleClient.CloseAsync(cancellationToken);
}
catch (OperationCanceledException)
{
// Cancellation is expected during shutdown — safe to ignore
}
LogModuleClientStopped({{ModuleName}}Constants.ModuleId);
}
private void OnStarted()
=> LogModuleStarted({{ModuleName}}Constants.ModuleId);
private void OnStopping()
=> LogModuleStopping({{ModuleName}}Constants.ModuleId);
private void OnStopped()
=> LogModuleStopped({{ModuleName}}Constants.ModuleId);
}

View File

@@ -0,0 +1,45 @@
namespace {{ModuleName}};
/// <summary>
/// {{ModuleName}}Service LoggerMessages.
/// </summary>
public sealed partial class {{ModuleName}}Service
{
private readonly ILogger<{{ModuleName}}Service> logger;
[LoggerMessage(
EventId = Atc.Azure.IoTEdge.LoggingEventIdConstants.ModuleStarted,
Level = LogLevel.Trace,
Message = "Successfully started module '{ModuleName}'")]
private partial void LogModuleStarted(string moduleName);
[LoggerMessage(
EventId = Atc.Azure.IoTEdge.LoggingEventIdConstants.ModuleStopping,
Level = LogLevel.Trace,
Message = "Stopping module '{ModuleName}'")]
private partial void LogModuleStopping(string moduleName);
[LoggerMessage(
EventId = Atc.Azure.IoTEdge.LoggingEventIdConstants.ModuleStopped,
Level = LogLevel.Trace,
Message = "Successfully stopped module '{moduleName}'")]
private partial void LogModuleStopped(string moduleName);
[LoggerMessage(
EventId = Atc.Azure.IoTEdge.LoggingEventIdConstants.ModuleClientStarted,
Level = LogLevel.Trace,
Message = "Successfully started moduleClient for module '{ModuleName}'")]
private partial void LogModuleClientStarted(string moduleName);
[LoggerMessage(
EventId = Atc.Azure.IoTEdge.LoggingEventIdConstants.ModuleClientStopped,
Level = LogLevel.Trace,
Message = "Successfully stopped moduleClient for module '{ModuleName}'")]
private partial void LogModuleClientStopped(string moduleName);
[LoggerMessage(
EventId = Atc.Azure.IoTEdge.LoggingEventIdConstants.ConnectionStatusChange,
Level = LogLevel.Debug,
Message = "Connection status changed: Status={Status}, Reason={Reason}")]
private partial void LogConnectionStatusChange(ConnectionStatus status, ConnectionStatusChangeReason reason);
}

View File

@@ -0,0 +1,111 @@
{
"modulesContent": {
"$edgeAgent": {
"properties.desired": {
"schemaVersion": "1.1",
"modules": {},
"runtime": {
"type": "docker",
"settings": {
"minDockerVersion": "v1.25",
"registryCredentials": {
"registryName": {
"username": "${ContainerRegistryUserName}",
"password": "${ContainerRegistryPassword}",
"address": "${ContainerRegistryLoginServer}"
}
}
}
},
"systemModules": {
"edgeAgent": {
"imagePullPolicy": "on-create",
"type": "docker",
"env": {
"storageFolder": {
"value": "/aziot/storage/"
},
"UpstreamProtocol": {
"value": "AMQPWS"
}
},
"settings": {
"image": "mcr.microsoft.com/azureiotedge-agent:1.5",
"createOptions": {
"HostConfig": {
"Binds": [
"/etc/aziot/storage/:/aziot/storage/"
],
"LogConfig": {
"Type": "json-file",
"Config": {
"max-size": "10m",
"max-file": "10"
}
}
}
}
}
},
"edgeHub": {
"imagePullPolicy": "on-create",
"type": "docker",
"env": {
"storageFolder": {
"value": "/aziot/storage/"
},
"UpstreamProtocol": {
"value": "AMQPWS"
}
},
"status": "running",
"restartPolicy": "always",
"startupOrder": 0,
"settings": {
"image": "mcr.microsoft.com/azureiotedge-hub:1.5",
"createOptions": {
"HostConfig": {
"Binds": [
"/etc/aziot/storage/:/aziot/storage/"
],
"LogConfig": {
"Type": "json-file",
"Config": {
"max-size": "10m",
"max-file": "10"
}
},
"PortBindings": {
"5671/tcp": [
{
"HostPort": "5671"
}
],
"8883/tcp": [
{
"HostPort": "8883"
}
],
"443/tcp": [
{
"HostPort": "443"
}
]
}
}
}
}
}
}
}
},
"$edgeHub": {
"properties.desired": {
"schemaVersion": "1.1",
"storeAndForwardConfiguration": {
"timeToLiveSecs": 86400
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
{
"profiles": {
"{{ModuleName}}": {
"commandName": "Project",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Standalone"
}
},
"Docker": {
"commandName": "Docker"
}
}
}

View File

@@ -0,0 +1,17 @@
{
"$schema-version": "0.0.1",
"description": "{{ModuleDescription}}",
"image": {
"repository": "{{CONTAINER_REGISTRY}}/{{modulename}}",
"tag": {
"version": "0.0.${BUILD_BUILDID}",
"platforms": {
"amd64": "./Dockerfile.amd64",
"amd64.debug": "./Dockerfile.amd64.debug"
}
},
"buildOptions": [],
"contextPath": "../../../"
},
"language": "csharp"
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Atc" Version="2.0.562" />
<PackageReference Include="Atc.Azure.IoTEdge" Version="1.0.177" />
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.42.3" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.11" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
</ItemGroup>
{{CONTRACTS_PROJECT_REFERENCE}}
<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
</ItemGroup>
</Project>