diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/20221004121605_Add EdgeDevice.Designer.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20221004121605_Add EdgeDevice.Designer.cs
new file mode 100644
index 000000000..4f65ac584
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20221004121605_Add EdgeDevice.Designer.cs
@@ -0,0 +1,440 @@
+//
+using System;
+using AzureIoTHub.Portal.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace AzureIoTHub.Portal.Infrastructure.Migrations
+{
+ [DbContext(typeof(PortalDbContext))]
+ [Migration("20221004121605_Add EdgeDevice")]
+ partial class AddEdgeDevice
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.Device", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("DeviceModelId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsConnected")
+ .HasColumnType("boolean");
+
+ b.Property("IsEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("StatusUpdatedTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Version")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceModel", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ABPRelaxMode")
+ .HasColumnType("boolean");
+
+ b.Property("AppEUI")
+ .HasColumnType("text");
+
+ b.Property("ClassType")
+ .HasColumnType("integer");
+
+ b.Property("Deduplication")
+ .HasColumnType("integer");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Downlink")
+ .HasColumnType("boolean");
+
+ b.Property("IsBuiltin")
+ .HasColumnType("boolean");
+
+ b.Property("KeepAliveTimeout")
+ .HasColumnType("integer");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("PreferredWindow")
+ .HasColumnType("integer");
+
+ b.Property("RXDelay")
+ .HasColumnType("integer");
+
+ b.Property("SensorDecoder")
+ .HasColumnType("text");
+
+ b.Property("SupportLoRaFeatures")
+ .HasColumnType("boolean");
+
+ b.Property("UseOTAA")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("DeviceModels");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceModelCommand", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("Confirmed")
+ .HasColumnType("boolean");
+
+ b.Property("DeviceModelId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Frame")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsBuiltin")
+ .HasColumnType("boolean");
+
+ b.Property("Port")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("DeviceModelCommands");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceModelProperty", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsWritable")
+ .HasColumnType("boolean");
+
+ b.Property("ModelId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Order")
+ .HasColumnType("integer");
+
+ b.Property("PropertyType")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("DeviceModelProperties");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceTag", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("Label")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Required")
+ .HasColumnType("boolean");
+
+ b.Property("Searchable")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("DeviceTags");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceTagValue", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("DeviceId")
+ .HasColumnType("text");
+
+ b.Property("EdgeDeviceId")
+ .HasColumnType("text");
+
+ b.Property("LorawanDeviceId")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("EdgeDeviceId");
+
+ b.HasIndex("LorawanDeviceId");
+
+ b.ToTable("DeviceTagValues");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.EdgeDevice", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("DeviceModelId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("NbDevices")
+ .HasColumnType("integer");
+
+ b.Property("NbModules")
+ .HasColumnType("integer");
+
+ b.Property("Scope")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Version")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("EdgeDevices");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.EdgeDeviceModel", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("EdgeDeviceModels");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.EdgeDeviceModelCommand", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("EdgeDeviceModelId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ModuleName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("EdgeDeviceModelCommands");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.LorawanDevice", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ABPRelaxMode")
+ .HasColumnType("boolean");
+
+ b.Property("AlreadyLoggedInOnce")
+ .HasColumnType("boolean");
+
+ b.Property("AppEUI")
+ .HasColumnType("text");
+
+ b.Property("AppKey")
+ .HasColumnType("text");
+
+ b.Property("AppSKey")
+ .HasColumnType("text");
+
+ b.Property("ClassType")
+ .HasColumnType("integer");
+
+ b.Property("DataRate")
+ .HasColumnType("text");
+
+ b.Property("Deduplication")
+ .HasColumnType("integer");
+
+ b.Property("DevAddr")
+ .HasColumnType("text");
+
+ b.Property("DeviceModelId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Downlink")
+ .HasColumnType("boolean");
+
+ b.Property("FCntDownStart")
+ .HasColumnType("integer");
+
+ b.Property("FCntResetCounter")
+ .HasColumnType("integer");
+
+ b.Property("FCntUpStart")
+ .HasColumnType("integer");
+
+ b.Property("GatewayID")
+ .HasColumnType("text");
+
+ b.Property("IsConnected")
+ .HasColumnType("boolean");
+
+ b.Property("IsEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("KeepAliveTimeout")
+ .HasColumnType("integer");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("NbRep")
+ .HasColumnType("text");
+
+ b.Property("NwkSKey")
+ .HasColumnType("text");
+
+ b.Property("PreferredWindow")
+ .HasColumnType("integer");
+
+ b.Property("RX1DROffset")
+ .HasColumnType("integer");
+
+ b.Property("RX2DataRate")
+ .HasColumnType("integer");
+
+ b.Property("RXDelay")
+ .HasColumnType("integer");
+
+ b.Property("ReportedRX1DROffset")
+ .HasColumnType("text");
+
+ b.Property("ReportedRX2DataRate")
+ .HasColumnType("text");
+
+ b.Property("ReportedRXDelay")
+ .HasColumnType("text");
+
+ b.Property("SensorDecoder")
+ .HasColumnType("text");
+
+ b.Property("StatusUpdatedTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Supports32BitFCnt")
+ .HasColumnType("boolean");
+
+ b.Property("TxPower")
+ .HasColumnType("text");
+
+ b.Property("UseOTAA")
+ .HasColumnType("boolean");
+
+ b.Property("Version")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("LorawanDevices");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceTagValue", b =>
+ {
+ b.HasOne("AzureIoTHub.Portal.Domain.Entities.Device", null)
+ .WithMany("Tags")
+ .HasForeignKey("DeviceId");
+
+ b.HasOne("AzureIoTHub.Portal.Domain.Entities.EdgeDevice", null)
+ .WithMany("Tags")
+ .HasForeignKey("EdgeDeviceId");
+
+ b.HasOne("AzureIoTHub.Portal.Domain.Entities.LorawanDevice", null)
+ .WithMany("Tags")
+ .HasForeignKey("LorawanDeviceId");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.Device", b =>
+ {
+ b.Navigation("Tags");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.EdgeDevice", b =>
+ {
+ b.Navigation("Tags");
+ });
+
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.LorawanDevice", b =>
+ {
+ b.Navigation("Tags");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/20221004121605_Add EdgeDevice.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20221004121605_Add EdgeDevice.cs
new file mode 100644
index 000000000..e160ca2ae
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20221004121605_Add EdgeDevice.cs
@@ -0,0 +1,55 @@
+// Copyright (c) CGI France. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+#nullable disable
+
+namespace AzureIoTHub.Portal.Infrastructure.Migrations
+{
+ using Microsoft.EntityFrameworkCore.Migrations;
+
+ public partial class AddEdgeDevice : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ _ = migrationBuilder.AddColumn(
+ name: "EdgeDeviceId",
+ table: "DeviceTagValues",
+ type: "text",
+ nullable: true);
+
+ _ = migrationBuilder.CreateTable(
+ name: "EdgeDevices",
+ columns: table => new
+ {
+ Id = table.Column(type: "text", nullable: false),
+ Name = table.Column(type: "text", nullable: false),
+ DeviceModelId = table.Column(type: "text", nullable: false),
+ Version = table.Column(type: "integer", nullable: false),
+ IsEnabled = table.Column(type: "boolean", nullable: false),
+ Scope = table.Column(type: "text", nullable: false),
+ NbDevices = table.Column(type: "integer", nullable: false),
+ NbModules = table.Column(type: "integer", nullable: false)
+ },
+ constraints: table =>
+ {
+ _ = table.PrimaryKey("PK_EdgeDevices", x => x.Id);
+ });
+
+ _ = migrationBuilder.CreateIndex(name: "IX_DeviceTagValues_EdgeDeviceId", table: "DeviceTagValues", column: "EdgeDeviceId");
+
+ _ = migrationBuilder.AddForeignKey(name: "FK_DeviceTagValues_EdgeDevices_EdgeDeviceId", table: "DeviceTagValues", column: "EdgeDeviceId", principalTable: "EdgeDevices", principalColumn: "Id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ _ = migrationBuilder.DropForeignKey(name: "FK_DeviceTagValues_EdgeDevices_EdgeDeviceId", table: "DeviceTagValues");
+
+ _ = migrationBuilder.DropTable(
+ name: "EdgeDevices");
+
+ _ = migrationBuilder.DropIndex(name: "IX_DeviceTagValues_EdgeDeviceId", table: "DeviceTagValues");
+
+ _ = migrationBuilder.DropColumn(name: "EdgeDeviceId", table: "DeviceTagValues");
+ }
+ }
+}
diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs
index 195f59bb7..de64f2bca 100644
--- a/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs
+++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs
@@ -226,6 +226,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property("DeviceId")
.HasColumnType("text");
+ b.Property("EdgeDeviceId")
+ .HasColumnType("text");
+
b.Property("LorawanDeviceId")
.HasColumnType("text");
@@ -241,11 +244,47 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("DeviceId");
+ b.HasIndex("EdgeDeviceId");
+
b.HasIndex("LorawanDeviceId");
b.ToTable("DeviceTagValues");
});
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.EdgeDevice", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("DeviceModelId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("NbDevices")
+ .HasColumnType("integer");
+
+ b.Property("NbModules")
+ .HasColumnType("integer");
+
+ b.Property("Scope")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Version")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("EdgeDevices");
+ });
+
modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.EdgeDeviceModel", b =>
{
b.Property("Id")
@@ -405,6 +444,10 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.WithMany("Tags")
.HasForeignKey("DeviceId");
+ b.HasOne("AzureIoTHub.Portal.Domain.Entities.EdgeDevice", null)
+ .WithMany("Tags")
+ .HasForeignKey("EdgeDeviceId");
+
b.HasOne("AzureIoTHub.Portal.Domain.Entities.LorawanDevice", null)
.WithMany("Tags")
.HasForeignKey("LorawanDeviceId");
@@ -415,6 +458,11 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Navigation("Tags");
});
+ modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.EdgeDevice", b =>
+ {
+ b.Navigation("Tags");
+ });
+
modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.LorawanDevice", b =>
{
b.Navigation("Tags");
diff --git a/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs b/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs
index b84c9a7fc..daed92731 100644
--- a/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs
+++ b/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs
@@ -14,6 +14,7 @@ public class PortalDbContext : DbContext
public DbSet DeviceModelCommands { get; set; }
public DbSet DeviceModels { get; set; }
public DbSet Devices { get; set; }
+ public DbSet EdgeDevices { get; set; }
public DbSet LorawanDevices { get; set; }
public DbSet DeviceTagValues { get; set; }
public DbSet EdgeDeviceModels { get; set; }
diff --git a/src/AzureIoTHub.Portal.Infrastructure/Repositories/EdgeDeviceRepository.cs b/src/AzureIoTHub.Portal.Infrastructure/Repositories/EdgeDeviceRepository.cs
new file mode 100644
index 000000000..2c720aa00
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Infrastructure/Repositories/EdgeDeviceRepository.cs
@@ -0,0 +1,25 @@
+// Copyright (c) CGI France. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace AzureIoTHub.Portal.Infrastructure.Repositories
+{
+ using System.Threading.Tasks;
+ using AzureIoTHub.Portal.Domain.Entities;
+ using AzureIoTHub.Portal.Domain.Repositories;
+ using Microsoft.EntityFrameworkCore;
+
+ public class EdgeDeviceRepository : GenericRepository, IEdgeDeviceRepository
+ {
+ public EdgeDeviceRepository(PortalDbContext context) : base(context)
+ {
+ }
+
+ public override Task GetByIdAsync(object id)
+ {
+ return this.context.Set()
+ .Include(device => device.Tags)
+ .Where(device => device.Id.Equals(id.ToString()))
+ .FirstOrDefaultAsync();
+ }
+ }
+}
diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/EdgeDeviceRepositoryTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/EdgeDeviceRepositoryTest.cs
new file mode 100644
index 000000000..af21f9911
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/EdgeDeviceRepositoryTest.cs
@@ -0,0 +1,60 @@
+// Copyright (c) CGI France. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace AzureIoTHub.Portal.Tests.Unit.Infrastructure.Repositories
+{
+ using System.Linq;
+ using System.Threading.Tasks;
+ using AutoFixture;
+ using AzureIoTHub.Portal.Domain.Entities;
+ using AzureIoTHub.Portal.Infrastructure.Repositories;
+ using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases;
+ using FluentAssertions;
+ using NUnit.Framework;
+
+ public class EdgeDeviceRepositoryTest : BackendUnitTest
+ {
+ private EdgeDeviceRepository edgeDeviceRepository;
+
+ public override void Setup()
+ {
+ base.Setup();
+
+ this.edgeDeviceRepository = new EdgeDeviceRepository(DbContext);
+ }
+
+ [Test]
+ public async Task GetAllShouldReturnExpectedEdgeDevices()
+ {
+ // Arrange
+ var expectedEdgeDevices = Fixture.CreateMany(5).ToList();
+
+ await DbContext.AddRangeAsync(expectedEdgeDevices);
+
+ _ = await DbContext.SaveChangesAsync();
+
+ //Act
+ var result = this.edgeDeviceRepository.GetAll().ToList();
+
+ // Assert
+ _ = result.Should().BeEquivalentTo(expectedEdgeDevices);
+ }
+
+ [Test]
+ public async Task GetByIdAsyncExistingDeviceReturnsExpectedEdgeDevice()
+ {
+ // Arrange
+ var expectedEdgeDevices = Fixture.Create();
+
+ _ = DbContext.Add(expectedEdgeDevices);
+
+ _ = await DbContext.SaveChangesAsync();
+
+ //Act
+ var result = await this.edgeDeviceRepository.GetByIdAsync(expectedEdgeDevices.Id);
+
+ // Assert
+ _ = result.Should().BeEquivalentTo(expectedEdgeDevices);
+ }
+ }
+}
diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncEdgeDeviceJobTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncEdgeDeviceJobTest.cs
new file mode 100644
index 000000000..065131c91
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncEdgeDeviceJobTest.cs
@@ -0,0 +1,205 @@
+// Copyright (c) CGI France. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace AzureIoTHub.Portal.Tests.Unit.Server.Jobs
+{
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+ using AutoFixture;
+ using AzureIoTHub.Portal.Domain;
+ using AzureIoTHub.Portal.Domain.Entities;
+ using AzureIoTHub.Portal.Domain.Repositories;
+ using AzureIoTHub.Portal.Server.Jobs;
+ using AzureIoTHub.Portal.Server.Services;
+ using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases;
+ using Microsoft.Azure.Devices.Shared;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Logging;
+ using Moq;
+ using NUnit.Framework;
+ using Quartz;
+
+ public class SyncEdgeDeviceJobTest : BackendUnitTest
+ {
+ private IJob syncEdgeDeviceJob;
+
+ private Mock mockExternalDeviceService;
+ private Mock mockEdgeDeviceRepository;
+ private Mock mockDeviceTagValueRepository;
+ private Mock mockEdgeDeviceModelRepository;
+ private Mock mockUnitOfWork;
+ private Mock mockEdgeDevicesService;
+ private Mock> mockLogger;
+
+ public override void Setup()
+ {
+ base.Setup();
+
+ this.mockExternalDeviceService = MockRepository.Create();
+ this.mockEdgeDeviceRepository = MockRepository.Create();
+ this.mockDeviceTagValueRepository = MockRepository.Create();
+ this.mockEdgeDeviceModelRepository = MockRepository.Create();
+ this.mockUnitOfWork = MockRepository.Create();
+ this.mockEdgeDevicesService = MockRepository.Create();
+ this.mockLogger = MockRepository.Create>();
+
+ _ = ServiceCollection.AddSingleton(this.mockExternalDeviceService.Object);
+ _ = ServiceCollection.AddSingleton(this.mockEdgeDeviceRepository.Object);
+ _ = ServiceCollection.AddSingleton(this.mockDeviceTagValueRepository.Object);
+ _ = ServiceCollection.AddSingleton(this.mockEdgeDeviceModelRepository.Object);
+ _ = ServiceCollection.AddSingleton(this.mockUnitOfWork.Object);
+ _ = ServiceCollection.AddSingleton(this.mockEdgeDevicesService.Object);
+ _ = ServiceCollection.AddSingleton();
+
+ Services = ServiceCollection.BuildServiceProvider();
+
+ this.syncEdgeDeviceJob = Services.GetRequiredService();
+ }
+
+ [Test]
+ public async Task ExecuteNewEdgeDeviceDeviceCreated()
+ {
+ // Arrange
+ var mockJobExecutionContext = MockRepository.Create();
+
+ var expectedDeviceModel = Fixture.Create();
+
+ var expectedTwinDevice = new Twin
+ {
+ DeviceId = Fixture.Create(),
+ Tags = new TwinCollection
+ {
+ ["modelId"] = expectedDeviceModel.Id,
+ ["deviceName"] = Fixture.Create(),
+ ["test"] = Fixture.Create()
+ },
+ Capabilities = new DeviceCapabilities{ IotEdge = true }
+ };
+
+ _ = this.mockExternalDeviceService
+ .Setup(x => x.GetAllEdgeDevice(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.Is(x => x == 100)))
+ .ReturnsAsync(new PaginationResult
+ {
+ Items = new List
+ {
+ expectedTwinDevice
+ },
+ TotalItems = 1
+ });
+
+ _ = this.mockExternalDeviceService
+ .Setup(x => x.GetDeviceTwinWithModule(It.Is(c => c.Equals(expectedTwinDevice.DeviceId, System.StringComparison.Ordinal))))
+ .ReturnsAsync(expectedTwinDevice);
+
+ _ = this.mockExternalDeviceService
+ .Setup(x => x.GetDeviceTwinWithEdgeHubModule(It.Is(c => c.Equals(expectedTwinDevice.DeviceId, System.StringComparison.Ordinal))))
+ .ReturnsAsync(expectedTwinDevice);
+
+ _ = this.mockEdgeDeviceModelRepository
+ .Setup(x => x.GetByIdAsync(expectedDeviceModel.Id))
+ .ReturnsAsync(expectedDeviceModel);
+
+ _ = this.mockEdgeDeviceRepository.Setup(x => x.GetByIdAsync(expectedTwinDevice.DeviceId)).
+ ReturnsAsync(value: null);
+
+ _ = this.mockEdgeDeviceRepository.Setup(x => x.InsertAsync(It.IsAny()))
+ .Returns(Task.CompletedTask);
+
+ _ = this.mockUnitOfWork.Setup(work => work.SaveAsync())
+ .Returns(Task.CompletedTask);
+
+ // Act
+ await this.syncEdgeDeviceJob.Execute(mockJobExecutionContext.Object);
+
+ // Assert
+ MockRepository.VerifyAll();
+ }
+
+ [Test]
+ public async Task ExecuteExistingEdgeDeviceWithGreeterVersionDeviceUpdated()
+ {
+ // Arrange
+ var mockJobExecutionContext = MockRepository.Create();
+
+ var expectedDeviceModel = Fixture.Create();
+
+ var expectedTwinDevice = new Twin
+ {
+ DeviceId = Fixture.Create(),
+ Tags = new TwinCollection
+ {
+ ["modelId"] = expectedDeviceModel.Id,
+ ["deviceName"] = Fixture.Create()
+ },
+ Capabilities = new DeviceCapabilities{ IotEdge = true },
+ Version = 2
+ };
+
+ var existingDevice = new EdgeDevice
+ {
+ Id = expectedTwinDevice.DeviceId,
+ Version = 1,
+ Tags = new List
+ {
+ new()
+ {
+ Id = Fixture.Create(),
+ Name = Fixture.Create(),
+ Value = Fixture.Create()
+ }
+ }
+ };
+
+ _ = this.mockExternalDeviceService
+ .Setup(x => x.GetAllEdgeDevice(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.Is(x => x == 100)))
+ .ReturnsAsync(new PaginationResult
+ {
+ Items = new List
+ {
+ expectedTwinDevice
+ },
+ TotalItems = 1
+ });
+
+ _ = this.mockExternalDeviceService
+ .Setup(x => x.GetDeviceTwinWithModule(It.Is(c => c.Equals(expectedTwinDevice.DeviceId, System.StringComparison.Ordinal))))
+ .ReturnsAsync(expectedTwinDevice);
+
+ _ = this.mockExternalDeviceService
+ .Setup(x => x.GetDeviceTwinWithEdgeHubModule(It.Is(c => c.Equals(expectedTwinDevice.DeviceId, System.StringComparison.Ordinal))))
+ .ReturnsAsync(expectedTwinDevice);
+
+ _ = this.mockEdgeDeviceModelRepository
+ .Setup(x => x.GetByIdAsync(expectedDeviceModel.Id))
+ .ReturnsAsync(expectedDeviceModel);
+
+ _ = this.mockEdgeDeviceRepository.Setup(x => x.GetByIdAsync(expectedTwinDevice.DeviceId)).
+ ReturnsAsync(existingDevice);
+
+ this.mockDeviceTagValueRepository.Setup(repository => repository.Delete(It.IsAny()))
+ .Verifiable();
+
+ this.mockEdgeDeviceRepository.Setup(repository => repository.Update(It.IsAny()))
+ .Verifiable();
+
+ _ = this.mockUnitOfWork.Setup(work => work.SaveAsync())
+ .Returns(Task.CompletedTask);
+
+ // Act
+ await this.syncEdgeDeviceJob.Execute(mockJobExecutionContext.Object);
+
+ // Assert
+ MockRepository.VerifyAll();
+ }
+ }
+}
diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeDeviceServiceTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeDeviceServiceTest.cs
index f44de73eb..71af181a0 100644
--- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeDeviceServiceTest.cs
+++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/EdgeDeviceServiceTest.cs
@@ -128,14 +128,9 @@ public async Task GetEdgeDeviceShouldReturnValue()
1, 2
};
- var mockQuery = this.mockRepository.Create();
- _ = mockQuery.Setup(c => c.GetNextAsTwinAsync())
- .ReturnsAsync(new[] { edgeHubTwin });
-
- _ = this.mockRegistryManager.Setup(c => c.CreateQuery(
- It.Is(x => x == $"SELECT * FROM devices.modules WHERE devices.modules.moduleId = '$edgeHub' AND deviceId in ['{expectedDevice.DeviceId}']"),
- It.Is(x => x == 1)))
- .Returns(mockQuery.Object);
+ _ = this.mockDeviceService
+ .Setup(x => x.GetDeviceTwinWithEdgeHubModule(It.Is(c => c.Equals(expectedDevice.DeviceId, StringComparison.Ordinal))))
+ .ReturnsAsync(edgeHubTwin);
_ = this.mockEdgeDeviceMapper
.Setup(x => x.CreateEdgeDevice(It.Is(c => c.DeviceId.Equals(expectedDevice.DeviceId, StringComparison.Ordinal)),
diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/ExternalDeviceServiceTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/ExternalDeviceServiceTests.cs
index 7fe8c42eb..9193abcae 100644
--- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/ExternalDeviceServiceTests.cs
+++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Services/ExternalDeviceServiceTests.cs
@@ -18,6 +18,7 @@ namespace AzureIoTHub.Portal.Tests.Unit.Server.Services
using Microsoft.Azure.Devices;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Extensions.Logging;
+ using Microsoft.Graph.ExternalConnectors;
using Models.v10;
using Moq;
using Newtonsoft.Json;
@@ -828,6 +829,58 @@ public async Task GetDeviceTwinWithModuleStateUnderTestExpectedBehavior()
this.mockRepository.VerifyAll();
}
+ [Test]
+ public async Task GetDeviceTwinWithEdgeHubModuleStateUnderTestExpectedBehavior()
+ {
+ // Arrange
+ var service = CreateService();
+ var deviceId = Guid.NewGuid().ToString();
+
+ var mockQuery = this.mockRepository.Create();
+
+ _ = mockQuery.Setup(c => c.GetNextAsTwinAsync())
+ .ReturnsAsync(new Twin[]
+ {
+ new Twin(Guid.NewGuid().ToString())
+ });
+
+ _ = this.mockRegistryManager.Setup(c => c.CreateQuery(
+ It.Is(x => x == $"SELECT * FROM devices.modules WHERE devices.modules.moduleId = '$edgeHub' AND deviceId in ['{deviceId}']"), It.Is(x => x == 1)))
+ .Returns(mockQuery.Object);
+
+ // Act
+ var result = await service.GetDeviceTwinWithEdgeHubModule(deviceId);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.IsAssignableFrom(result);
+ this.mockRepository.VerifyAll();
+ }
+
+ [Test]
+ public async Task GetDeviceTwinWithEdgeHubModule()
+ {
+ // Arrange
+ var service = CreateService();
+ var deviceId = Guid.NewGuid().ToString();
+
+ var mockQuery = this.mockRepository.Create();
+
+ _ = mockQuery.Setup(c => c.GetNextAsTwinAsync())
+ .ThrowsAsync(new Exception(""));
+
+ _ = this.mockRegistryManager.Setup(c => c.CreateQuery(
+ It.Is(x => x == $"SELECT * FROM devices.modules WHERE devices.modules.moduleId = '$edgeHub' AND deviceId in ['{deviceId}']"), It.Is(x => x == 1)))
+ .Returns(mockQuery.Object);
+
+ // Act
+ var act = async () => await service.GetDeviceTwinWithEdgeHubModule(deviceId);
+
+ // Assert
+ _ = await act.Should().ThrowAsync();
+ this.mockRepository.VerifyAll();
+ }
+
[TestCase(false, DeviceStatus.Enabled)]
[TestCase(true, DeviceStatus.Enabled)]
[TestCase(false, DeviceStatus.Disabled)]
diff --git a/src/AzureIoTHub.Portal/Server/Jobs/SyncEdgeDeviceJob.cs b/src/AzureIoTHub.Portal/Server/Jobs/SyncEdgeDeviceJob.cs
new file mode 100644
index 000000000..8765e8d9a
--- /dev/null
+++ b/src/AzureIoTHub.Portal/Server/Jobs/SyncEdgeDeviceJob.cs
@@ -0,0 +1,136 @@
+// Copyright (c) CGI France. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace AzureIoTHub.Portal.Server.Jobs
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+ using AutoMapper;
+ using AzureIoTHub.Portal.Domain;
+ using AzureIoTHub.Portal.Domain.Entities;
+ using AzureIoTHub.Portal.Domain.Repositories;
+ using AzureIoTHub.Portal.Server.Services;
+ using Microsoft.Azure.Devices.Shared;
+ using Microsoft.Extensions.Logging;
+ using Quartz;
+
+ [DisallowConcurrentExecution]
+ public class SyncEdgeDeviceJob : IJob
+ {
+ private readonly IExternalDeviceService externalDeviceService;
+ private readonly IEdgeDeviceRepository edgeDeviceRepository;
+ private readonly IDeviceTagValueRepository deviceTagValueRepository;
+ private readonly IEdgeDeviceModelRepository edgeDeviceModelRepository;
+ private readonly IMapper mapper;
+ private readonly IUnitOfWork unitOfWork;
+ private readonly ILogger logger;
+
+ private readonly IEdgeDevicesService edgeDevicesService;
+
+ private const string ModelId = "modelId";
+
+ public SyncEdgeDeviceJob(IExternalDeviceService externalDeviceService,
+ IEdgeDeviceModelRepository edgeDeviceModelRepository,
+ IEdgeDevicesService edgeDevicesService,
+ IEdgeDeviceRepository edgeDeviceRepository,
+ IDeviceTagValueRepository deviceTagValueRepository,
+ IMapper mapper,
+ IUnitOfWork unitOfWork,
+ ILogger logger)
+ {
+ this.edgeDeviceModelRepository = edgeDeviceModelRepository;
+ this.externalDeviceService = externalDeviceService;
+ this.edgeDeviceRepository = edgeDeviceRepository;
+ this.deviceTagValueRepository = deviceTagValueRepository;
+ this.mapper = mapper;
+ this.unitOfWork = unitOfWork;
+ this.logger = logger;
+ this.edgeDevicesService = edgeDevicesService;
+ }
+
+ public async Task Execute(IJobExecutionContext context)
+ {
+ try
+ {
+ this.logger.LogInformation("Start of sync devices job");
+
+ await SyncEdgeDevices();
+
+ this.logger.LogInformation("End of sync devices job");
+ }
+ catch (Exception e)
+ {
+ this.logger.LogError(e, "Sync devices job has failed");
+ }
+ }
+
+ private async Task SyncEdgeDevices()
+ {
+ foreach (var twin in await GetTwinDevices())
+ {
+ var deviceModel = await this.edgeDeviceModelRepository.GetByIdAsync(twin.Tags[ModelId]?.ToString() ?? string.Empty);
+
+ if (deviceModel == null)
+ {
+ this.logger.LogWarning($"The device with wont be synched, its model id {twin.Tags[ModelId]?.ToString()} doesn't exist");
+ continue;
+ }
+
+ await CreateOrUpdateDevice(twin);
+ }
+
+ await this.unitOfWork.SaveAsync();
+ }
+
+ private async Task> GetTwinDevices()
+ {
+ var twins = new List();
+ var continuationToken = string.Empty;
+
+ int totalTwinDevices;
+ do
+ {
+ var result = await this.externalDeviceService.GetAllEdgeDevice(continuationToken: continuationToken, pageSize: 100);
+ twins.AddRange(result.Items);
+
+ totalTwinDevices = result.TotalItems;
+ continuationToken = result.NextPage;
+
+ } while (totalTwinDevices > twins.Count);
+
+ return twins;
+ }
+
+ private async Task CreateOrUpdateDevice(Twin twin)
+ {
+ var twinWithModule = await this.externalDeviceService.GetDeviceTwinWithModule(twin.DeviceId);
+ var twinWithClient = await this.externalDeviceService.GetDeviceTwinWithEdgeHubModule(twin.DeviceId);
+
+ var device = this.mapper.Map(twin,opts =>
+ {
+ opts.Items["TwinModules"] = twinWithModule;
+ opts.Items["TwinClient"] = twinWithClient;
+ }) ;
+
+ var deviceEntity = await this.edgeDeviceRepository.GetByIdAsync(device.Id);
+
+ if (deviceEntity == null)
+ {
+ await this.edgeDeviceRepository.InsertAsync(device);
+ }
+ else
+ {
+ if (deviceEntity.Version >= device.Version) return;
+
+ foreach (var deviceTagEntity in deviceEntity.Tags)
+ {
+ this.deviceTagValueRepository.Delete(deviceTagEntity.Id);
+ }
+
+ _ = this.mapper.Map(device, deviceEntity);
+ this.edgeDeviceRepository.Update(deviceEntity);
+ }
+ }
+ }
+}
diff --git a/src/AzureIoTHub.Portal/Server/Mappers/EdgeDeviceProfile.cs b/src/AzureIoTHub.Portal/Server/Mappers/EdgeDeviceProfile.cs
new file mode 100644
index 000000000..7577aa544
--- /dev/null
+++ b/src/AzureIoTHub.Portal/Server/Mappers/EdgeDeviceProfile.cs
@@ -0,0 +1,52 @@
+// Copyright (c) CGI France. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace AzureIoTHub.Portal.Server.Mappers
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text.Json;
+ using AutoMapper;
+ using AzureIoTHub.Portal.Domain.Entities;
+ using AzureIoTHub.Portal.Server.Helpers;
+ using Microsoft.Azure.Devices.Shared;
+ using Newtonsoft.Json.Linq;
+
+ public class EdgeDeviceProfile : Profile
+ {
+ public EdgeDeviceProfile()
+ {
+ _ = CreateMap();
+
+ _ = CreateMap()
+ .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.DeviceId))
+ .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Tags["deviceName"]))
+ .ForMember(dest => dest.DeviceModelId, opts => opts.MapFrom(src => src.Tags["modelId"]))
+ .ForMember(dest => dest.Version, opts => opts.MapFrom(src => src.Version))
+ .ForMember(dest => dest.IsEnabled, opts => opts.MapFrom(src => src.Status == Microsoft.Azure.Devices.DeviceStatus.Enabled))
+ .ForMember(dest => dest.Tags, opts => opts.MapFrom(src => GetTags(src)))
+ .ForMember(dest => dest.Scope, opts => opts.MapFrom(src => src.DeviceScope))
+ .ForMember(dest => dest.NbDevices, opts => opts.MapFrom((src, _, _, context) => GetNbConnectedDevice((Twin)context.Items["TwinClient"])))
+ .ForMember(dest => dest.NbModules, opts => opts.MapFrom((src, _, _, context) => DeviceHelper.RetrieveNbModuleCount((Twin)context.Items["TwinModules"], src.DeviceId)));
+ }
+
+ private static ICollection GetTags(Twin twin)
+ {
+ return (JsonSerializer.Deserialize>(twin.Tags.ToJson()) ?? new Dictionary())
+ .Where(tag => tag.Key is not "modelId" and not "deviceName")
+ .Select(tag => new DeviceTagValue
+ {
+ Name = tag.Key,
+ Value = tag.Value.ToString() ?? string.Empty
+ })
+ .ToList();
+ }
+
+ private static int GetNbConnectedDevice(Twin twin)
+ {
+ var reportedProperties = JObject.Parse(twin.Properties.Reported.ToJson());
+
+ return reportedProperties.TryGetValue("clients", out var clients) ? clients.Count() : 0;
+ }
+ }
+}
diff --git a/src/AzureIoTHub.Portal/Server/Services/EdgeDevicesService.cs b/src/AzureIoTHub.Portal/Server/Services/EdgeDevicesService.cs
index e1e9fd09c..640e91a71 100644
--- a/src/AzureIoTHub.Portal/Server/Services/EdgeDevicesService.cs
+++ b/src/AzureIoTHub.Portal/Server/Services/EdgeDevicesService.cs
@@ -255,8 +255,7 @@ public async Task GetEdgeDeviceCredentials(string edgeDev
/// The device identifier.
private async Task RetrieveNbConnectedDevice(string deviceId)
{
- var query = this.registryManager.CreateQuery($"SELECT * FROM devices.modules WHERE devices.modules.moduleId = '$edgeHub' AND deviceId in ['{deviceId}']", 1);
- var deviceWithClient = (await query.GetNextAsTwinAsync()).SingleOrDefault();
+ var deviceWithClient = await this.externalDevicesService.GetDeviceTwinWithEdgeHubModule(deviceId);
var reportedProperties = JObject.Parse(deviceWithClient.Properties.Reported.ToJson());
return reportedProperties.TryGetValue("clients", out var clients) ? clients.Count() : 0;
diff --git a/src/AzureIoTHub.Portal/Server/Services/ExternalDeviceService.cs b/src/AzureIoTHub.Portal/Server/Services/ExternalDeviceService.cs
index d851993d5..9a9d05b6d 100644
--- a/src/AzureIoTHub.Portal/Server/Services/ExternalDeviceService.cs
+++ b/src/AzureIoTHub.Portal/Server/Services/ExternalDeviceService.cs
@@ -44,7 +44,7 @@ public ExternalDeviceService(
}
///
- /// this function return a list of all edge device wthiout tags.
+ /// this function return a list of all edge device without modules.
///
///
///
@@ -315,6 +315,21 @@ public async Task GetDeviceTwinWithModule(string deviceId)
return null;
}
+ public async Task GetDeviceTwinWithEdgeHubModule(string deviceId)
+ {
+ var query = this.registryManager.CreateQuery($"SELECT * FROM devices.modules WHERE devices.modules.moduleId = '$edgeHub' AND deviceId in ['{deviceId}']", 1);
+
+ try
+ {
+ var devicesTwins = await query.GetNextAsTwinAsync();
+ return devicesTwins.ElementAt(0);
+ }
+ catch (Exception e)
+ {
+ throw new InternalServerErrorException($"Unable to get devices twins", e);
+ }
+ }
+
///
/// This function create a new device with his twin.
///
diff --git a/src/AzureIoTHub.Portal/Server/Services/IExternalDeviceService.cs b/src/AzureIoTHub.Portal/Server/Services/IExternalDeviceService.cs
index 5ff2b00a1..198beddac 100644
--- a/src/AzureIoTHub.Portal/Server/Services/IExternalDeviceService.cs
+++ b/src/AzureIoTHub.Portal/Server/Services/IExternalDeviceService.cs
@@ -16,6 +16,7 @@ public interface IExternalDeviceService
Task GetDeviceTwin(string deviceId);
Task GetDeviceTwinWithModule(string deviceId);
+ Task GetDeviceTwinWithEdgeHubModule(string deviceId);
Task CreateDeviceWithTwin(string deviceId, bool isEdge, Twin twin, DeviceStatus isEnabled);
diff --git a/src/AzureIoTHub.Portal/Server/Startup.cs b/src/AzureIoTHub.Portal/Server/Startup.cs
index 2b015afc4..84a3653d4 100644
--- a/src/AzureIoTHub.Portal/Server/Startup.cs
+++ b/src/AzureIoTHub.Portal/Server/Startup.cs
@@ -152,6 +152,7 @@ public void ConfigureServices(IServiceCollection services)
_ = services.AddScoped();
_ = services.AddScoped();
_ = services.AddScoped();
+ _ = services.AddScoped();
_ = services.AddScoped();
_ = services.AddScoped();
_ = services.AddScoped();
@@ -323,9 +324,15 @@ Specify the authorization token got from your IDP as a header.
.WithSimpleSchedule(s => s
.WithIntervalInMinutes(configuration.SyncDatabaseJobRefreshIntervalInMinutes)
.RepeatForever()));
- });
-
+ _ = q.AddJob(j => j.WithIdentity(nameof(SyncEdgeDeviceJob)))
+ .AddTrigger(t => t
+ .WithIdentity($"{nameof(SyncEdgeDeviceJob)}")
+ .ForJob(nameof(SyncEdgeDeviceJob))
+ .WithSimpleSchedule(s => s
+ .WithIntervalInMinutes(configuration.SyncDatabaseJobRefreshIntervalInMinutes)
+ .RepeatForever()));
+ });
// Add the Quartz.NET hosted service
_ = services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
diff --git a/src/AzureIoTHubPortal.Domain/Entities/EdgeDevice.cs b/src/AzureIoTHubPortal.Domain/Entities/EdgeDevice.cs
new file mode 100644
index 000000000..b4d0d3442
--- /dev/null
+++ b/src/AzureIoTHubPortal.Domain/Entities/EdgeDevice.cs
@@ -0,0 +1,41 @@
+// Copyright (c) CGI France. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace AzureIoTHub.Portal.Domain.Entities
+{
+ using AzureIoTHub.Portal.Domain.Base;
+
+ public class EdgeDevice : EntityBase
+ {
+ public string Name { get; set; }
+
+ public string DeviceModelId { get; set; }
+
+ public int Version { get; set; }
+
+ ///
+ /// true if this instance is enabled; otherwise, false.
+ ///
+ public bool IsEnabled { get; set; }
+
+ ///
+ /// The IoT Edge scope tag value.
+ ///
+ public string Scope { get; set; }
+
+ ///
+ /// The number of connected devices on IoT Edge device.
+ ///
+ public int NbDevices { get; set; }
+
+ ///
+ /// The number of modules on IoT Edge device.
+ ///
+ public int NbModules { get; set; }
+
+ ///
+ /// List of custom device tags and their values.
+ ///
+ public ICollection Tags { get; set; }
+ }
+}
diff --git a/src/AzureIoTHubPortal.Domain/Repositories/IEdgeDeviceRepository.cs b/src/AzureIoTHubPortal.Domain/Repositories/IEdgeDeviceRepository.cs
new file mode 100644
index 000000000..ef4db7f23
--- /dev/null
+++ b/src/AzureIoTHubPortal.Domain/Repositories/IEdgeDeviceRepository.cs
@@ -0,0 +1,11 @@
+// Copyright (c) CGI France. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace AzureIoTHub.Portal.Domain.Repositories
+{
+ using AzureIoTHub.Portal.Domain.Entities;
+
+ public interface IEdgeDeviceRepository : IRepository
+ {
+ }
+}