From c409a70e3265ff2973687c29e7354d51c876999a Mon Sep 17 00:00:00 2001
From: ben salim <salim.benahben@cgi.com>
Date: Mon, 19 Sep 2022 15:09:16 +0200
Subject: [PATCH 01/11] add new entities

---
 .../Entities/Device.cs                        |  40 +++++
 .../Entities/LorawanDevice.cs                 | 168 ++++++++++++++++++
 2 files changed, 208 insertions(+)
 create mode 100644 src/AzureIoTHubPortal.Domain/Entities/Device.cs
 create mode 100644 src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs

diff --git a/src/AzureIoTHubPortal.Domain/Entities/Device.cs b/src/AzureIoTHubPortal.Domain/Entities/Device.cs
new file mode 100644
index 000000000..9c44a66eb
--- /dev/null
+++ b/src/AzureIoTHubPortal.Domain/Entities/Device.cs
@@ -0,0 +1,40 @@
+// 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 Device : EntityBase
+    {
+        /// <summary>
+        /// The name of the device.
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// The model identifier.
+        /// </summary>
+        public string DeviceModelId { get; set; }
+
+        /// <summary>
+        ///   <c>true</c> if this instance is connected; otherwise, <c>false</c>.
+        /// </summary>
+        public bool IsConnected { get; set; }
+
+        /// <summary>
+        ///   <c>true</c> if this instance is enabled; otherwise, <c>false</c>.
+        /// </summary>
+        public bool IsEnabled { get; set; }
+
+        /// <summary>
+        /// The status updated time.
+        /// </summary>
+        public DateTime StatusUpdatedTime { get; set; }
+
+        /// <summary>
+        /// List of custom device tags and their values.
+        /// </summary>
+        public Dictionary<string, string> Tags { get; set; } = new();
+    }
+}
diff --git a/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
new file mode 100644
index 000000000..d92fd4856
--- /dev/null
+++ b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
@@ -0,0 +1,168 @@
+// 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.Models.v10.LoRaWAN;
+
+    public class LorawanDevice : Device
+    {
+        /// <summary>
+        /// A value indicating whether the device uses OTAA to authenticate to LoRaWAN Network, otherwise ABP
+        /// </summary>
+        public bool UseOTAA { get; set; }
+
+        /// <summary>
+        /// The OTAA App Key.
+        /// </summary>
+        public string AppKey { get; set; }
+
+        /// <summary>
+        /// The device OTAA Application EUI.
+        /// </summary>
+        public string AppEUI { get; set; }
+
+        /// <summary>
+        ///  The ABP AppSKey.
+        /// </summary>
+        public string AppSKey { get; set; }
+
+        /// <summary>
+        ///  The ABP NwkSKey.
+        /// </summary>
+        public string NwkSKey { get; set; }
+
+        /// <summary>
+        /// Unique identifier that allows
+        /// the device to be recognized.
+        /// </summary>
+        public string DevAddr { get; set; }
+
+        /// <summary>
+        /// A value indicating whether the device has already joined the platform.
+        /// </summary>
+        public bool AlreadyLoggedInOnce { get; set; }
+
+        /// <summary>
+        /// The Device Current Datarate,
+        /// This value will be only reported if you are using Adaptive Data Rate.
+        /// </summary>
+        public string DataRate { get; set; }
+
+        /// <summary>
+        /// The Device Current Transmit Power,
+        /// This value will be only reported if you are using Adaptive Data Rate.
+        /// </summary>
+        public string TxPower { get; set; }
+
+        /// <summary>
+        /// The Device Current repetition when transmitting.
+        /// E.g. if set to two, the device will transmit twice his upstream messages.
+        /// This value will be only reported if you are using Adaptive Data Rate.
+        /// </summary>
+        public string NbRep { get; set; }
+
+        /// <summary>
+        /// The Device Current Rx2Datarate.
+        /// </summary>
+        public string ReportedRX2DataRate { get; set; }
+
+        /// <summary>
+        /// The Device Current RX1DROffset.
+        /// </summary>
+        public string ReportedRX1DROffset { get; set; }
+
+        /// <summary>
+        /// The Device Current RXDelay.
+        /// </summary>
+        public string ReportedRXDelay { get; set; }
+
+        /// <summary>
+        /// The GatewayID of the device.
+        /// </summary>
+        public string GatewayID { get; set; }
+
+        /// <summary>
+        /// A value indicating whether the downlinks are enabled (True if not provided)
+        /// </summary>
+        public bool? Downlink { get; set; }
+
+        /// <summary>
+        /// The LoRa device class.
+        /// Default is A.
+        /// </summary>
+        public ClassType ClassType { get; set; }
+
+        /// <summary>
+        /// Allows setting the device preferred receive window (RX1 or RX2).
+        /// The default preferred receive window is 1.
+        /// </summary>
+        public int PreferredWindow { get; set; }
+
+        /// <summary>
+        /// Allows controlling the handling of duplicate messages received by multiple gateways.
+        /// The default is Drop.
+        /// </summary>
+        public DeduplicationMode Deduplication { get; set; }
+
+        /// <summary>
+        /// Allows setting an offset between received Datarate and retransmit datarate as specified in the LoRa Specifiations.
+        /// Valid for OTAA devices.
+        /// If an invalid value is provided the network server will use default value 0.
+        /// </summary>
+        public int? RX1DROffset { get; set; }
+
+        /// <summary>
+        /// Allows setting a custom Datarate for second receive windows.
+        /// Valid for OTAA devices.
+        /// If an invalid value is provided the network server will use default value 0 (DR0).
+        /// </summary>
+        public int? RX2DataRate { get; set; }
+
+        /// <summary>
+        /// Allows setting a custom wait time between receiving and transmission as specified in the specification.
+        /// </summary>
+        public int? RXDelay { get; set; }
+
+        /// <summary>
+        /// Allows to disable the relax mode when using ABP.
+        /// By default relaxed mode is enabled.
+        /// </summary>
+        public bool? ABPRelaxMode { get; set; }
+
+        /// <summary>
+        /// Allows to explicitly specify a frame counter up start value.
+        /// If the device joins, this value will be used to validate the first frame and initialize the server state for the device.
+        /// Default is 0.
+        /// </summary>
+        public int? FCntUpStart { get; set; }
+
+        /// <summary>
+        /// Allows to explicitly specify a frame counter down start value.
+        /// Default is 0.
+        /// </summary>
+        public int? FCntDownStart { get; set; }
+
+        /// <summary>
+        /// Allows to reset the frame counters to the FCntUpStart/FCntDownStart values respectively.
+        /// Default is 0.
+        /// </summary>
+        public int? FCntResetCounter { get; set; }
+
+        /// <summary>
+        /// Allow the usage of 32bit counters on your device.
+        /// </summary>
+        public bool? Supports32BitFCnt { get; set; }
+
+        /// <summary>
+        /// Allows defining a sliding expiration to the connection between the leaf device and IoT/Edge Hub.
+        /// The default is none, which causes the connection to not be dropped.
+        /// </summary>
+        public int? KeepAliveTimeout { get; set; }
+
+        /// <summary>
+        /// The sensor decoder API Url.
+        /// </summary>
+        public string SensorDecoder { get; set; }
+    }
+}

From b7eb9d9574a5d7db97b2f7888515f17f33ffb78c Mon Sep 17 00:00:00 2001
From: ben salim <salim.benahben@cgi.com>
Date: Mon, 19 Sep 2022 15:15:39 +0200
Subject: [PATCH 02/11] add repositories

---
 .../PortalDbContext.cs                            |  2 ++
 .../Repositories/DeviceRepository.cs              | 15 +++++++++++++++
 .../Repositories/LorawanDeviceRepository.cs       | 15 +++++++++++++++
 .../Repositories/IDeviceRepository.cs             | 11 +++++++++++
 .../Repositories/ILorawanDeviceRepository.cs      | 11 +++++++++++
 5 files changed, 54 insertions(+)
 create mode 100644 src/AzureIoTHub.Portal.Infrastructure/Repositories/DeviceRepository.cs
 create mode 100644 src/AzureIoTHub.Portal.Infrastructure/Repositories/LorawanDeviceRepository.cs
 create mode 100644 src/AzureIoTHubPortal.Domain/Repositories/IDeviceRepository.cs
 create mode 100644 src/AzureIoTHubPortal.Domain/Repositories/ILorawanDeviceRepository.cs

diff --git a/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs b/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs
index adb8bca34..ee82bd6ad 100644
--- a/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs
+++ b/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs
@@ -16,6 +16,8 @@ public class PortalDbContext : DbContext
         public DbSet<DeviceTag> DeviceTags { get; set; }
         public DbSet<DeviceModelCommand> DeviceModelCommands { get; set; }
         public DbSet<DeviceModel> DeviceModels { get; set; }
+        public DbSet<Device> Devices { get; set; }
+        public DbSet<LorawanDevice> LorawanDevices { get; set; }
         public DbSet<EdgeDeviceModel> EdgeDeviceModels { get; set; }
         public DbSet<EdgeDeviceModelCommand> EdgeDeviceModelCommands { get; set; }
 
diff --git a/src/AzureIoTHub.Portal.Infrastructure/Repositories/DeviceRepository.cs b/src/AzureIoTHub.Portal.Infrastructure/Repositories/DeviceRepository.cs
new file mode 100644
index 000000000..ae814479a
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Infrastructure/Repositories/DeviceRepository.cs
@@ -0,0 +1,15 @@
+// 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 AzureIoTHub.Portal.Domain.Repositories;
+    using Domain.Entities;
+
+    public class DeviceRepository : GenericRepository<Device>, IDeviceRepository
+    {
+        public DeviceRepository(PortalDbContext context) : base(context)
+        {
+        }
+    }
+}
diff --git a/src/AzureIoTHub.Portal.Infrastructure/Repositories/LorawanDeviceRepository.cs b/src/AzureIoTHub.Portal.Infrastructure/Repositories/LorawanDeviceRepository.cs
new file mode 100644
index 000000000..1ab71dd99
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Infrastructure/Repositories/LorawanDeviceRepository.cs
@@ -0,0 +1,15 @@
+// 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 AzureIoTHub.Portal.Domain.Repositories;
+    using Domain.Entities;
+
+    public class LorawanDeviceRepository : GenericRepository<LorawanDevice>, ILorawanDeviceRepository
+    {
+        public LorawanDeviceRepository(PortalDbContext context) : base(context)
+        {
+        }
+    }
+}
diff --git a/src/AzureIoTHubPortal.Domain/Repositories/IDeviceRepository.cs b/src/AzureIoTHubPortal.Domain/Repositories/IDeviceRepository.cs
new file mode 100644
index 000000000..8037dc49b
--- /dev/null
+++ b/src/AzureIoTHubPortal.Domain/Repositories/IDeviceRepository.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 Entities;
+
+    public interface IDeviceRepository : IRepository<Device>
+    {
+    }
+}
diff --git a/src/AzureIoTHubPortal.Domain/Repositories/ILorawanDeviceRepository.cs b/src/AzureIoTHubPortal.Domain/Repositories/ILorawanDeviceRepository.cs
new file mode 100644
index 000000000..1678577e8
--- /dev/null
+++ b/src/AzureIoTHubPortal.Domain/Repositories/ILorawanDeviceRepository.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 Entities;
+
+    public interface ILorawanDeviceRepository : IRepository<LorawanDevice>
+    {
+    }
+}

From 9266e08a39428e197cd9220c7800ad80d56ae54c Mon Sep 17 00:00:00 2001
From: ben salim <salim.benahben@cgi.com>
Date: Mon, 19 Sep 2022 16:07:45 +0200
Subject: [PATCH 03/11] create syncDevice job

---
 .../Server/Jobs/SyncDevicesJob.cs             | 40 +++++++++++++++++++
 src/AzureIoTHub.Portal/Server/Startup.cs      | 10 +++++
 .../Entities/Device.cs                        |  2 +
 .../Entities/LorawanDevice.cs                 | 36 ++++++++++++++++-
 4 files changed, 87 insertions(+), 1 deletion(-)
 create mode 100644 src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs

diff --git a/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs b/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
new file mode 100644
index 000000000..c267a3dfc
--- /dev/null
+++ b/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
@@ -0,0 +1,40 @@
+// 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.Collections.Generic;
+    using System.Threading.Tasks;
+    using AzureIoTHub.Portal.Server.Services;
+    using Microsoft.Azure.Devices.Shared;
+    using Quartz;
+
+    public class SyncDevicesJob : IJob
+    {
+        private readonly IDeviceService deviceService;
+
+        public SyncDevicesJob(IDeviceService deviceService)
+        {
+            this.deviceService = deviceService;
+        }
+
+        public async Task Execute(IJobExecutionContext context)
+        {
+            var twins = new List<Twin>();
+            var continuationToken = string.Empty;
+
+            int totalTwinDevices;
+            do
+            {
+                var result = await this.deviceService.GetAllDevice(continuationToken: continuationToken, pageSize: 100);
+                twins.AddRange(result.Items);
+
+                totalTwinDevices = result.TotalItems;
+                continuationToken = result.NextPage;
+
+            } while (totalTwinDevices > twins.Count);
+
+            throw new System.NotImplementedException();
+        }
+    }
+}
diff --git a/src/AzureIoTHub.Portal/Server/Startup.cs b/src/AzureIoTHub.Portal/Server/Startup.cs
index f36d0eede..750c6b794 100644
--- a/src/AzureIoTHub.Portal/Server/Startup.cs
+++ b/src/AzureIoTHub.Portal/Server/Startup.cs
@@ -18,6 +18,7 @@ namespace AzureIoTHub.Portal.Server
     using AzureIoTHub.Portal.Infrastructure.Factories;
     using AzureIoTHub.Portal.Infrastructure.Repositories;
     using AzureIoTHub.Portal.Infrastructure.Seeds;
+    using AzureIoTHub.Portal.Server.Jobs;
     using Extensions;
     using Hellang.Middleware.ProblemDetails;
     using Hellang.Middleware.ProblemDetails.Mvc;
@@ -312,6 +313,15 @@ Specify the authorization token got from your IDP as a header.
                 q.AddMetricsService<DeviceMetricExporterService, DeviceMetricLoaderService>(configuration);
                 q.AddMetricsService<EdgeDeviceMetricExporterService, EdgeDeviceMetricLoaderService>(configuration);
                 q.AddMetricsService<ConcentratorMetricExporterService, ConcentratorMetricLoaderService>(configuration);
+
+                _ = q.AddJob<SyncDevicesJob>(j => j.WithIdentity(nameof(SyncDevicesJob)))
+                    .AddTrigger(t => t
+                        .WithIdentity($"{nameof(SyncDevicesJob)}")
+                        .ForJob(nameof(SyncDevicesJob))
+                        .WithSimpleSchedule(s => s
+                            //.WithIntervalInSeconds(configuration.MetricExporterRefreshIntervalInSeconds)
+                            .WithIntervalInSeconds(30)
+                            .RepeatForever()));
             });
 
 
diff --git a/src/AzureIoTHubPortal.Domain/Entities/Device.cs b/src/AzureIoTHubPortal.Domain/Entities/Device.cs
index 9c44a66eb..775d223a5 100644
--- a/src/AzureIoTHubPortal.Domain/Entities/Device.cs
+++ b/src/AzureIoTHubPortal.Domain/Entities/Device.cs
@@ -3,6 +3,7 @@
 
 namespace AzureIoTHub.Portal.Domain.Entities
 {
+    using System.ComponentModel.DataAnnotations.Schema;
     using AzureIoTHub.Portal.Domain.Base;
 
     public class Device : EntityBase
@@ -35,6 +36,7 @@ public class Device : EntityBase
         /// <summary>
         /// List of custom device tags and their values.
         /// </summary>
+        [Column(TypeName = "jsonb")]
         public Dictionary<string, string> Tags { get; set; } = new();
     }
 }
diff --git a/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
index d92fd4856..87bfb58a9 100644
--- a/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
+++ b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
@@ -3,10 +3,44 @@
 
 namespace AzureIoTHub.Portal.Domain.Entities
 {
+    using System.ComponentModel.DataAnnotations.Schema;
+    using AzureIoTHub.Portal.Domain.Base;
     using AzureIoTHub.Portal.Models.v10.LoRaWAN;
 
-    public class LorawanDevice : Device
+    public class LorawanDevice : EntityBase
     {
+
+        /// <summary>
+        /// The name of the device.
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// The model identifier.
+        /// </summary>
+        public string DeviceModelId { get; set; }
+
+        /// <summary>
+        ///   <c>true</c> if this instance is connected; otherwise, <c>false</c>.
+        /// </summary>
+        public bool IsConnected { get; set; }
+
+        /// <summary>
+        ///   <c>true</c> if this instance is enabled; otherwise, <c>false</c>.
+        /// </summary>
+        public bool IsEnabled { get; set; }
+
+        /// <summary>
+        /// The status updated time.
+        /// </summary>
+        public DateTime StatusUpdatedTime { get; set; }
+
+        /// <summary>
+        /// List of custom device tags and their values.
+        /// </summary>
+        [Column(TypeName = "jsonb")]
+        public Dictionary<string, string> Tags { get; set; } = new();
+
         /// <summary>
         /// A value indicating whether the device uses OTAA to authenticate to LoRaWAN Network, otherwise ABP
         /// </summary>

From c197f7f1fbe488cd5b4a46f585d164c9c76fac24 Mon Sep 17 00:00:00 2001
From: ben salim <salim.benahben@cgi.com>
Date: Wed, 21 Sep 2022 16:45:07 +0200
Subject: [PATCH 04/11] update mapper

---
 .../Server/Jobs/SyncDevicesJob.cs             | 75 ++++++++++++++++++-
 .../Server/Mappers/DeviceProfile.cs           | 68 +++++++++++++++++
 src/AzureIoTHub.Portal/Server/Startup.cs      |  5 +-
 .../Entities/LorawanDevice.cs                 | 26 +++----
 4 files changed, 157 insertions(+), 17 deletions(-)
 create mode 100644 src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs

diff --git a/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs b/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
index c267a3dfc..ac2683a86 100644
--- a/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
+++ b/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
@@ -5,17 +5,37 @@ namespace AzureIoTHub.Portal.Server.Jobs
 {
     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 Quartz;
 
+    [DisallowConcurrentExecution]
     public class SyncDevicesJob : IJob
     {
         private readonly IDeviceService deviceService;
+        private readonly IDeviceModelRepository deviceModelRepository;
+        private readonly ILorawanDeviceRepository lorawanDeviceRepository;
+        private readonly IDeviceRepository deviceRepository;
+        private readonly IMapper mapper;
+        private readonly IUnitOfWork unitOfWork;
 
-        public SyncDevicesJob(IDeviceService deviceService)
+        public SyncDevicesJob(IDeviceService deviceService,
+            IDeviceModelRepository deviceModelRepository,
+            ILorawanDeviceRepository lorawanDeviceRepository,
+            IDeviceRepository deviceRepository,
+            IMapper mapper,
+            IUnitOfWork unitOfWork)
         {
             this.deviceService = deviceService;
+            this.deviceModelRepository = deviceModelRepository;
+            this.lorawanDeviceRepository = lorawanDeviceRepository;
+            this.deviceRepository = deviceRepository;
+            this.mapper = mapper;
+            this.unitOfWork = unitOfWork;
         }
 
         public async Task Execute(IJobExecutionContext context)
@@ -26,7 +46,7 @@ public async Task Execute(IJobExecutionContext context)
             int totalTwinDevices;
             do
             {
-                var result = await this.deviceService.GetAllDevice(continuationToken: continuationToken, pageSize: 100);
+                var result = await this.deviceService.GetAllDevice(continuationToken: continuationToken,excludeDeviceType: "LoRa Concentrator", pageSize: 100);
                 twins.AddRange(result.Items);
 
                 totalTwinDevices = result.TotalItems;
@@ -34,7 +54,56 @@ public async Task Execute(IJobExecutionContext context)
 
             } while (totalTwinDevices > twins.Count);
 
-            throw new System.NotImplementedException();
+            foreach (var twin in twins)
+            {
+                var model = await this.deviceModelRepository.GetByIdAsync(twin.Tags["modelId"]?.ToString());
+
+                if (model == null)
+                {
+                    continue;
+                }
+
+                if (model.SupportLoRaFeatures)
+                {
+                    var device = this.mapper.Map<LorawanDevice>(twin);
+
+                    var lorawanDeviceEntity = await this.lorawanDeviceRepository.GetByIdAsync(device.Id);
+                    if (lorawanDeviceEntity == null)
+                    {
+                        await this.lorawanDeviceRepository.InsertAsync(device);
+                    }
+                    else
+                    {
+                        if (lorawanDeviceEntity.StatusUpdatedTime < device.StatusUpdatedTime)
+                        {
+                            _ = this.mapper.Map(device, lorawanDeviceEntity);
+                            this.lorawanDeviceRepository.Update(lorawanDeviceEntity);
+                        }
+                    }
+                }
+                else
+                {
+                    var device = this.mapper.Map<Device>(twin);
+
+                    var deviceEntity = await this.deviceRepository.GetByIdAsync(device.Id);
+                    if (deviceEntity == null)
+                    {
+                        await this.deviceRepository.InsertAsync(device);
+                    }
+                    else
+                    {
+                        if (deviceEntity.StatusUpdatedTime < device.StatusUpdatedTime)
+                        {
+                            _ = this.mapper.Map(device, deviceEntity);
+                            this.deviceRepository.Update(deviceEntity);
+                        }
+                    }
+                }
+            }
+
+            // save entity
+            await this.unitOfWork.SaveAsync();
+
         }
     }
 }
diff --git a/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs b/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs
new file mode 100644
index 000000000..f0d40fa28
--- /dev/null
+++ b/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs
@@ -0,0 +1,68 @@
+// 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 AutoMapper;
+    using AzureIoTHub.Portal.Domain.Entities;
+    using AzureIoTHub.Portal.Models.v10.LoRaWAN;
+    using Microsoft.Azure.Devices.Shared;
+
+    public class DeviceProfile : Profile
+    {
+        public DeviceProfile()
+        {
+            _ = CreateMap<Twin, Device>()
+                .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.IsConnected, opts => opts.MapFrom(src => src.ConnectionState == Microsoft.Azure.Devices.DeviceConnectionState.Connected))
+                .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)));
+
+            _ = CreateMap<Twin, LorawanDevice>()
+                .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.IsConnected, opts => opts.MapFrom(src => src.ConnectionState == Microsoft.Azure.Devices.DeviceConnectionState.Connected))
+                .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.UseOTAA, opts => opts.MapFrom(src => !string.IsNullOrEmpty(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.AppEUI)))))
+                .ForMember(dest => dest.AppKey, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.AppKey))))
+                .ForMember(dest => dest.AppEUI, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.AppEUI))))
+                .ForMember(dest => dest.AppSKey, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.AppSKey))))
+                .ForMember(dest => dest.NwkSKey, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.NwkSKey))))
+                .ForMember(dest => dest.DevAddr, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.DevAddr))))
+                .ForMember(dest => dest.AlreadyLoggedInOnce, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, "DevAddr") != null))
+                .ForMember(dest => dest.DataRate, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.DataRate))))
+                .ForMember(dest => dest.TxPower, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.TxPower))))
+                .ForMember(dest => dest.NbRep, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.NbRep))))
+                .ForMember(dest => dest.ReportedRX2DataRate, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.ReportedRX2DataRate))))
+                .ForMember(dest => dest.ReportedRX1DROffset, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.ReportedRX1DROffset))))
+                .ForMember(dest => dest.ReportedRXDelay, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.ReportedRXDelay))))
+                .ForMember(dest => dest.GatewayID, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.GatewayID))));
+            //.ForMember(dest => dest.Downlink, opts => opts.MapFrom(src => bool.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.Downlink)), out bool result)));
+        }
+
+        private static Dictionary<string, string> GetTags(Twin twin)
+        {
+            var customTags = new Dictionary<string, string>();
+
+            if (twin.Tags != null)
+            {
+                var tagList  = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(twin.Tags.ToJson());
+
+                foreach (var tag in tagList)
+                {
+                    if (tag.Key is not "modelId" and not "deviceName")
+                    {
+                        customTags.Add(tag.Key, tag.Value.ToString());
+                    }
+                }
+            }
+
+            return customTags;
+        }
+    }
+}
diff --git a/src/AzureIoTHub.Portal/Server/Startup.cs b/src/AzureIoTHub.Portal/Server/Startup.cs
index 750c6b794..3c1aff26f 100644
--- a/src/AzureIoTHub.Portal/Server/Startup.cs
+++ b/src/AzureIoTHub.Portal/Server/Startup.cs
@@ -151,6 +151,8 @@ public void ConfigureServices(IServiceCollection services)
             _ = services.AddScoped<IDeviceModelPropertiesRepository, DeviceModelPropertiesRepository>();
             _ = services.AddScoped<IDeviceTagRepository, DeviceTagRepository>();
             _ = services.AddScoped<IDeviceModelRepository, DeviceModelRepository>();
+            _ = services.AddScoped<IDeviceRepository, DeviceRepository>();
+            _ = services.AddScoped<ILorawanDeviceRepository, LorawanDeviceRepository>();
             _ = services.AddScoped<IDeviceModelCommandRepository, DeviceModelCommandRepository>();
 
             _ = services.AddMudServices();
@@ -276,6 +278,7 @@ Specify the authorization token got from your IDP as a header.
                 mc.AddProfile(new DeviceTagProfile());
                 mc.AddProfile(new DeviceModelProfile());
                 mc.AddProfile(new DeviceModelCommandProfile());
+                mc.AddProfile(new DeviceProfile());
             });
 
             var mapper = mapperConfig.CreateMapper();
@@ -320,7 +323,7 @@ Specify the authorization token got from your IDP as a header.
                         .ForJob(nameof(SyncDevicesJob))
                         .WithSimpleSchedule(s => s
                             //.WithIntervalInSeconds(configuration.MetricExporterRefreshIntervalInSeconds)
-                            .WithIntervalInSeconds(30)
+                            .WithIntervalInSeconds(3000)
                             .RepeatForever()));
             });
 
diff --git a/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
index 87bfb58a9..da07e206a 100644
--- a/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
+++ b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
@@ -49,28 +49,28 @@ public class LorawanDevice : EntityBase
         /// <summary>
         /// The OTAA App Key.
         /// </summary>
-        public string AppKey { get; set; }
+        public string? AppKey { get; set; }
 
         /// <summary>
         /// The device OTAA Application EUI.
         /// </summary>
-        public string AppEUI { get; set; }
+        public string? AppEUI { get; set; }
 
         /// <summary>
         ///  The ABP AppSKey.
         /// </summary>
-        public string AppSKey { get; set; }
+        public string? AppSKey { get; set; }
 
         /// <summary>
         ///  The ABP NwkSKey.
         /// </summary>
-        public string NwkSKey { get; set; }
+        public string? NwkSKey { get; set; }
 
         /// <summary>
         /// Unique identifier that allows
         /// the device to be recognized.
         /// </summary>
-        public string DevAddr { get; set; }
+        public string? DevAddr { get; set; }
 
         /// <summary>
         /// A value indicating whether the device has already joined the platform.
@@ -81,40 +81,40 @@ public class LorawanDevice : EntityBase
         /// The Device Current Datarate,
         /// This value will be only reported if you are using Adaptive Data Rate.
         /// </summary>
-        public string DataRate { get; set; }
+        public string? DataRate { get; set; }
 
         /// <summary>
         /// The Device Current Transmit Power,
         /// This value will be only reported if you are using Adaptive Data Rate.
         /// </summary>
-        public string TxPower { get; set; }
+        public string? TxPower { get; set; }
 
         /// <summary>
         /// The Device Current repetition when transmitting.
         /// E.g. if set to two, the device will transmit twice his upstream messages.
         /// This value will be only reported if you are using Adaptive Data Rate.
         /// </summary>
-        public string NbRep { get; set; }
+        public string? NbRep { get; set; }
 
         /// <summary>
         /// The Device Current Rx2Datarate.
         /// </summary>
-        public string ReportedRX2DataRate { get; set; }
+        public string? ReportedRX2DataRate { get; set; }
 
         /// <summary>
         /// The Device Current RX1DROffset.
         /// </summary>
-        public string ReportedRX1DROffset { get; set; }
+        public string? ReportedRX1DROffset { get; set; }
 
         /// <summary>
         /// The Device Current RXDelay.
         /// </summary>
-        public string ReportedRXDelay { get; set; }
+        public string? ReportedRXDelay { get; set; }
 
         /// <summary>
         /// The GatewayID of the device.
         /// </summary>
-        public string GatewayID { get; set; }
+        public string? GatewayID { get; set; }
 
         /// <summary>
         /// A value indicating whether the downlinks are enabled (True if not provided)
@@ -197,6 +197,6 @@ public class LorawanDevice : EntityBase
         /// <summary>
         /// The sensor decoder API Url.
         /// </summary>
-        public string SensorDecoder { get; set; }
+        public string? SensorDecoder { get; set; }
     }
 }

From e2eaa8b324ed281e8be6d56fad6c5e32d113ba29 Mon Sep 17 00:00:00 2001
From: ben salim <salim.benahben@cgi.com>
Date: Thu, 22 Sep 2022 11:42:23 +0200
Subject: [PATCH 05/11] update device job

---
 .../Server/Jobs/SyncDevicesJob.cs             |  4 +--
 .../Server/Mappers/DeviceProfile.cs           | 35 +++++++++++++++++--
 .../Entities/Device.cs                        |  2 ++
 .../Entities/LorawanDevice.cs                 |  2 ++
 4 files changed, 39 insertions(+), 4 deletions(-)

diff --git a/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs b/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
index ac2683a86..b6c6fff0d 100644
--- a/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
+++ b/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
@@ -74,7 +74,7 @@ public async Task Execute(IJobExecutionContext context)
                     }
                     else
                     {
-                        if (lorawanDeviceEntity.StatusUpdatedTime < device.StatusUpdatedTime)
+                        if (lorawanDeviceEntity.Version < device.Version)
                         {
                             _ = this.mapper.Map(device, lorawanDeviceEntity);
                             this.lorawanDeviceRepository.Update(lorawanDeviceEntity);
@@ -92,7 +92,7 @@ public async Task Execute(IJobExecutionContext context)
                     }
                     else
                     {
-                        if (deviceEntity.StatusUpdatedTime < device.StatusUpdatedTime)
+                        if (deviceEntity.Version < device.Version)
                         {
                             _ = this.mapper.Map(device, deviceEntity);
                             this.deviceRepository.Update(deviceEntity);
diff --git a/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs b/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs
index f0d40fa28..4a7096d0e 100644
--- a/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs
+++ b/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs
@@ -3,6 +3,7 @@
 
 namespace AzureIoTHub.Portal.Server.Mappers
 {
+    using System;
     using System.Collections.Generic;
     using AutoMapper;
     using AzureIoTHub.Portal.Domain.Entities;
@@ -17,6 +18,7 @@ public DeviceProfile()
                 .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.IsConnected, opts => opts.MapFrom(src => src.ConnectionState == Microsoft.Azure.Devices.DeviceConnectionState.Connected))
                 .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)));
@@ -25,6 +27,7 @@ public DeviceProfile()
                 .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.IsConnected, opts => opts.MapFrom(src => src.ConnectionState == Microsoft.Azure.Devices.DeviceConnectionState.Connected))
                 .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)))
@@ -41,8 +44,21 @@ public DeviceProfile()
                 .ForMember(dest => dest.ReportedRX2DataRate, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.ReportedRX2DataRate))))
                 .ForMember(dest => dest.ReportedRX1DROffset, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.ReportedRX1DROffset))))
                 .ForMember(dest => dest.ReportedRXDelay, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.ReportedRXDelay))))
-                .ForMember(dest => dest.GatewayID, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.GatewayID))));
-            //.ForMember(dest => dest.Downlink, opts => opts.MapFrom(src => bool.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.Downlink)), out bool result)));
+                .ForMember(dest => dest.GatewayID, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.GatewayID))))
+                .ForMember(dest => dest.Downlink, opts => opts.MapFrom(src => GetDesiredPropertyAsBooleanValue(src, nameof(LoRaDeviceDetails.Downlink))))
+                .ForMember(dest => dest.RX1DROffset, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.RX1DROffset))))
+                .ForMember(dest => dest.RX2DataRate, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.RX2DataRate))))
+                .ForMember(dest => dest.RXDelay, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.RXDelay))))
+                .ForMember(dest => dest.ABPRelaxMode, opts => opts.MapFrom(src => GetDesiredPropertyAsBooleanValue(src, nameof(LoRaDeviceDetails.ABPRelaxMode))))
+                .ForMember(dest => dest.FCntUpStart, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.FCntUpStart))))
+                .ForMember(dest => dest.FCntDownStart, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.FCntDownStart))))
+                .ForMember(dest => dest.FCntResetCounter, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.FCntResetCounter))))
+                .ForMember(dest => dest.Supports32BitFCnt, opts => opts.MapFrom(src => GetDesiredPropertyAsBooleanValue(src, nameof(LoRaDeviceDetails.Supports32BitFCnt))))
+                .ForMember(dest => dest.KeepAliveTimeout, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.KeepAliveTimeout))))
+                .ForMember(dest => dest.SensorDecoder, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.SensorDecoder))))
+                .ForMember(dest => dest.Deduplication, opts => opts.MapFrom(src => GetDesiredPropertyAsEnum<DeduplicationMode>(src, nameof(LoRaDeviceDetails.Deduplication))))
+                .ForMember(dest => dest.PreferredWindow, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.PreferredWindow)) ?? 0))
+                .ForMember(dest => dest.ClassType, opts => opts.MapFrom(src => GetDesiredPropertyAsEnum<ClassType>(src, nameof(LoRaDeviceDetails.ClassType))));
         }
 
         private static Dictionary<string, string> GetTags(Twin twin)
@@ -64,5 +80,20 @@ private static Dictionary<string, string> GetTags(Twin twin)
 
             return customTags;
         }
+
+        private static bool? GetDesiredPropertyAsBooleanValue(Twin twin, string propertyName)
+        {
+            return bool.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, propertyName), out var boolResult) ? boolResult : null;
+        }
+
+        private static int? GetDesiredPropertyAsIntegerValue(Twin twin, string propertyName)
+        {
+            return int.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, propertyName), out var numberStyles) ? numberStyles : null;
+        }
+
+        private static TEnum GetDesiredPropertyAsEnum<TEnum>(Twin twin, string propertyName) where TEnum : struct, IConvertible
+        {
+            return Enum.TryParse<TEnum>(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, propertyName), out var result) ? result : default;
+        }
     }
 }
diff --git a/src/AzureIoTHubPortal.Domain/Entities/Device.cs b/src/AzureIoTHubPortal.Domain/Entities/Device.cs
index 775d223a5..6c4368af3 100644
--- a/src/AzureIoTHubPortal.Domain/Entities/Device.cs
+++ b/src/AzureIoTHubPortal.Domain/Entities/Device.cs
@@ -28,6 +28,8 @@ public class Device : EntityBase
         /// </summary>
         public bool IsEnabled { get; set; }
 
+        public int Version { get; set; }
+
         /// <summary>
         /// The status updated time.
         /// </summary>
diff --git a/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
index da07e206a..7577240d5 100644
--- a/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
+++ b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
@@ -30,6 +30,8 @@ public class LorawanDevice : EntityBase
         /// </summary>
         public bool IsEnabled { get; set; }
 
+        public int Version { get; set; }
+
         /// <summary>
         /// The status updated time.
         /// </summary>

From 1a972c2da76eae9ed48ec9351208fec56088a0d2 Mon Sep 17 00:00:00 2001
From: ben salim <salim.benahben@cgi.com>
Date: Thu, 22 Sep 2022 13:39:48 +0200
Subject: [PATCH 06/11] resolve #1030

---
 .../ConfigHandlerBase.cs                      |   2 +
 .../DevelopmentConfigHandler.cs               |   2 +
 ...2_Add Device and LorawanDevice.Designer.cs | 345 ++++++++++++++++++
 ...0922094312_Add Device and LorawanDevice.cs |  90 +++++
 .../PortalDbContextModelSnapshot.cs           | 155 +++++++-
 .../ProductionConfigHandler.cs                |   2 +
 .../Server/Mappers/DeviceProfile.cs           |   3 +
 src/AzureIoTHub.Portal/Server/Startup.cs      |   4 +-
 src/AzureIoTHubPortal.Domain/ConfigHandler.cs |   2 +
 9 files changed, 602 insertions(+), 3 deletions(-)
 create mode 100644 src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.Designer.cs
 create mode 100644 src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.cs

diff --git a/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs b/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs
index efc729b54..044d1d558 100644
--- a/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs
+++ b/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs
@@ -39,6 +39,8 @@ internal abstract class ConfigHandlerBase : ConfigHandler
         internal const string MetricExporterRefreshIntervalKey = "Metrics:ExporterRefreshIntervalInSeconds";
         internal const string MetricLoaderRefreshIntervalKey = "Metrics:LoaderRefreshIntervalInMinutes";
 
+        internal const string SyncDevicesJobRefreshIntervalKey = "Job:SyncDeviceJobRefreshIntervalInSeconds";
+
         internal const string IdeasEnabledKey = "Ideas:Enabled";
         internal const string IdeasUrlKey = "Ideas:Url";
         internal const string IdeasAuthenticationHeaderKey = "Ideas:Authentication:Header";
diff --git a/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs b/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs
index 1b0b52708..2c671a617 100644
--- a/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs
+++ b/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs
@@ -16,6 +16,8 @@ internal DevelopmentConfigHandler(IConfiguration config)
 
         public override string PortalName => this.config[PortalNameKey];
 
+        public override int SyncDevicesJobRefreshIntervalInSeconds => this.config.GetValue(SyncDevicesJobRefreshIntervalKey, 300);
+
         public override int MetricExporterRefreshIntervalInSeconds => this.config.GetValue(MetricExporterRefreshIntervalKey, 30);
 
         public override int MetricLoaderRefreshIntervalInMinutes => this.config.GetValue(MetricLoaderRefreshIntervalKey, 10);
diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.Designer.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.Designer.cs
new file mode 100644
index 000000000..4ff1a743c
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.Designer.cs	
@@ -0,0 +1,345 @@
+// <auto-generated />
+using System;
+using System.Collections.Generic;
+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("20220922094312_Add Device and LorawanDevice")]
+    partial class AddDeviceandLorawanDevice
+    {
+        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<string>("Id")
+                        .HasColumnType("text");
+
+                    b.Property<string>("DeviceModelId")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<bool>("IsConnected")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsEnabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<DateTime>("StatusUpdatedTime")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<Dictionary<string, string>>("Tags")
+                        .IsRequired()
+                        .HasColumnType("jsonb");
+
+                    b.Property<int>("Version")
+                        .HasColumnType("integer");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Devices");
+                });
+
+            modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceModel", b =>
+                {
+                    b.Property<string>("Id")
+                        .HasColumnType("text");
+
+                    b.Property<bool?>("ABPRelaxMode")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("AppEUI")
+                        .HasColumnType("text");
+
+                    b.Property<int?>("Deduplication")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Description")
+                        .HasColumnType("text");
+
+                    b.Property<bool?>("Downlink")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsBuiltin")
+                        .HasColumnType("boolean");
+
+                    b.Property<int?>("KeepAliveTimeout")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<int?>("PreferredWindow")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("RXDelay")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("SensorDecoder")
+                        .HasColumnType("text");
+
+                    b.Property<bool>("SupportLoRaFeatures")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool?>("UseOTAA")
+                        .HasColumnType("boolean");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("DeviceModels");
+                });
+
+            modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceModelCommand", b =>
+                {
+                    b.Property<string>("Id")
+                        .HasColumnType("text");
+
+                    b.Property<bool>("Confirmed")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("DeviceModelId")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("Frame")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<bool>("IsBuiltin")
+                        .HasColumnType("boolean");
+
+                    b.Property<int>("Port")
+                        .HasColumnType("integer");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("DeviceModelCommands");
+                });
+
+            modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceModelProperty", b =>
+                {
+                    b.Property<string>("Id")
+                        .HasColumnType("text");
+
+                    b.Property<string>("DisplayName")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<bool>("IsWritable")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("ModelId")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("PropertyType")
+                        .HasColumnType("integer");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("DeviceModelProperties");
+                });
+
+            modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceTag", b =>
+                {
+                    b.Property<string>("Id")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Label")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<bool>("Required")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("Searchable")
+                        .HasColumnType("boolean");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("DeviceTags");
+                });
+
+            modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.EdgeDeviceModel", b =>
+                {
+                    b.Property<string>("Id")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Description")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("EdgeDeviceModels");
+                });
+
+            modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.EdgeDeviceModelCommand", b =>
+                {
+                    b.Property<string>("Id")
+                        .HasColumnType("text");
+
+                    b.Property<string>("EdgeDeviceModelId")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("EdgeDeviceModelCommands");
+                });
+
+            modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.LorawanDevice", b =>
+                {
+                    b.Property<string>("Id")
+                        .HasColumnType("text");
+
+                    b.Property<bool?>("ABPRelaxMode")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("AlreadyLoggedInOnce")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("AppEUI")
+                        .HasColumnType("text");
+
+                    b.Property<string>("AppKey")
+                        .HasColumnType("text");
+
+                    b.Property<string>("AppSKey")
+                        .HasColumnType("text");
+
+                    b.Property<int>("ClassType")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("DataRate")
+                        .HasColumnType("text");
+
+                    b.Property<int>("Deduplication")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("DevAddr")
+                        .HasColumnType("text");
+
+                    b.Property<string>("DeviceModelId")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<bool?>("Downlink")
+                        .HasColumnType("boolean");
+
+                    b.Property<int?>("FCntDownStart")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("FCntResetCounter")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("FCntUpStart")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("GatewayID")
+                        .HasColumnType("text");
+
+                    b.Property<bool>("IsConnected")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsEnabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<int?>("KeepAliveTimeout")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("NbRep")
+                        .HasColumnType("text");
+
+                    b.Property<string>("NwkSKey")
+                        .HasColumnType("text");
+
+                    b.Property<int>("PreferredWindow")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("RX1DROffset")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("RX2DataRate")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("RXDelay")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("ReportedRX1DROffset")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ReportedRX2DataRate")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ReportedRXDelay")
+                        .HasColumnType("text");
+
+                    b.Property<string>("SensorDecoder")
+                        .HasColumnType("text");
+
+                    b.Property<DateTime>("StatusUpdatedTime")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<bool?>("Supports32BitFCnt")
+                        .HasColumnType("boolean");
+
+                    b.Property<Dictionary<string, string>>("Tags")
+                        .IsRequired()
+                        .HasColumnType("jsonb");
+
+                    b.Property<string>("TxPower")
+                        .HasColumnType("text");
+
+                    b.Property<bool>("UseOTAA")
+                        .HasColumnType("boolean");
+
+                    b.Property<int>("Version")
+                        .HasColumnType("integer");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("LorawanDevices");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.cs
new file mode 100644
index 000000000..8158e9abb
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.cs	
@@ -0,0 +1,90 @@
+// 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 System;
+    using System.Collections.Generic;
+    using Microsoft.EntityFrameworkCore.Migrations;
+
+    public partial class AddDeviceandLorawanDevice : Migration
+    {
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            _ = migrationBuilder.CreateTable(
+                name: "Devices",
+                columns: table => new
+                {
+                    Id = table.Column<string>(type: "text", nullable: false),
+                    Name = table.Column<string>(type: "text", nullable: false),
+                    DeviceModelId = table.Column<string>(type: "text", nullable: false),
+                    IsConnected = table.Column<bool>(type: "boolean", nullable: false),
+                    IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
+                    Version = table.Column<int>(type: "integer", nullable: false),
+                    StatusUpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+                    Tags = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: false)
+                },
+                constraints: table =>
+                {
+                    _ = table.PrimaryKey("PK_Devices", x => x.Id);
+                });
+
+            _ = migrationBuilder.CreateTable(
+                name: "LorawanDevices",
+                columns: table => new
+                {
+                    Id = table.Column<string>(type: "text", nullable: false),
+                    Name = table.Column<string>(type: "text", nullable: false),
+                    DeviceModelId = table.Column<string>(type: "text", nullable: false),
+                    IsConnected = table.Column<bool>(type: "boolean", nullable: false),
+                    IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
+                    Version = table.Column<int>(type: "integer", nullable: false),
+                    StatusUpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+                    Tags = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: false),
+                    UseOTAA = table.Column<bool>(type: "boolean", nullable: false),
+                    AppKey = table.Column<string>(type: "text", nullable: true),
+                    AppEUI = table.Column<string>(type: "text", nullable: true),
+                    AppSKey = table.Column<string>(type: "text", nullable: true),
+                    NwkSKey = table.Column<string>(type: "text", nullable: true),
+                    DevAddr = table.Column<string>(type: "text", nullable: true),
+                    AlreadyLoggedInOnce = table.Column<bool>(type: "boolean", nullable: false),
+                    DataRate = table.Column<string>(type: "text", nullable: true),
+                    TxPower = table.Column<string>(type: "text", nullable: true),
+                    NbRep = table.Column<string>(type: "text", nullable: true),
+                    ReportedRX2DataRate = table.Column<string>(type: "text", nullable: true),
+                    ReportedRX1DROffset = table.Column<string>(type: "text", nullable: true),
+                    ReportedRXDelay = table.Column<string>(type: "text", nullable: true),
+                    GatewayID = table.Column<string>(type: "text", nullable: true),
+                    Downlink = table.Column<bool>(type: "boolean", nullable: true),
+                    ClassType = table.Column<int>(type: "integer", nullable: false),
+                    PreferredWindow = table.Column<int>(type: "integer", nullable: false),
+                    Deduplication = table.Column<int>(type: "integer", nullable: false),
+                    RX1DROffset = table.Column<int>(type: "integer", nullable: true),
+                    RX2DataRate = table.Column<int>(type: "integer", nullable: true),
+                    RXDelay = table.Column<int>(type: "integer", nullable: true),
+                    ABPRelaxMode = table.Column<bool>(type: "boolean", nullable: true),
+                    FCntUpStart = table.Column<int>(type: "integer", nullable: true),
+                    FCntDownStart = table.Column<int>(type: "integer", nullable: true),
+                    FCntResetCounter = table.Column<int>(type: "integer", nullable: true),
+                    Supports32BitFCnt = table.Column<bool>(type: "boolean", nullable: true),
+                    KeepAliveTimeout = table.Column<int>(type: "integer", nullable: true),
+                    SensorDecoder = table.Column<string>(type: "text", nullable: true)
+                },
+                constraints: table =>
+                {
+                    _ = table.PrimaryKey("PK_LorawanDevices", x => x.Id);
+                });
+        }
+
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            _ = migrationBuilder.DropTable(
+                name: "Devices");
+
+            _ = migrationBuilder.DropTable(
+                name: "LorawanDevices");
+        }
+    }
+}
diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs
index 91c84a2e3..4b6f0f11f 100644
--- a/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs
+++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs
@@ -1,5 +1,6 @@
 // <auto-generated />
 using System;
+using System.Collections.Generic;
 using AzureIoTHub.Portal.Infrastructure;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -17,11 +18,45 @@ protected override void BuildModel(ModelBuilder modelBuilder)
         {
 #pragma warning disable 612, 618
             modelBuilder
-                .HasAnnotation("ProductVersion", "6.0.8")
+                .HasAnnotation("ProductVersion", "6.0.9")
                 .HasAnnotation("Relational:MaxIdentifierLength", 63);
 
             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 
+            modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.Device", b =>
+                {
+                    b.Property<string>("Id")
+                        .HasColumnType("text");
+
+                    b.Property<string>("DeviceModelId")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<bool>("IsConnected")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsEnabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<DateTime>("StatusUpdatedTime")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<Dictionary<string, string>>("Tags")
+                        .IsRequired()
+                        .HasColumnType("jsonb");
+
+                    b.Property<int>("Version")
+                        .HasColumnType("integer");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Devices");
+                });
+
             modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceModel", b =>
                 {
                     b.Property<string>("Id")
@@ -184,6 +219,124 @@ protected override void BuildModel(ModelBuilder modelBuilder)
 
                     b.ToTable("EdgeDeviceModelCommands");
                 });
+
+            modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.LorawanDevice", b =>
+                {
+                    b.Property<string>("Id")
+                        .HasColumnType("text");
+
+                    b.Property<bool?>("ABPRelaxMode")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("AlreadyLoggedInOnce")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("AppEUI")
+                        .HasColumnType("text");
+
+                    b.Property<string>("AppKey")
+                        .HasColumnType("text");
+
+                    b.Property<string>("AppSKey")
+                        .HasColumnType("text");
+
+                    b.Property<int>("ClassType")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("DataRate")
+                        .HasColumnType("text");
+
+                    b.Property<int>("Deduplication")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("DevAddr")
+                        .HasColumnType("text");
+
+                    b.Property<string>("DeviceModelId")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<bool?>("Downlink")
+                        .HasColumnType("boolean");
+
+                    b.Property<int?>("FCntDownStart")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("FCntResetCounter")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("FCntUpStart")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("GatewayID")
+                        .HasColumnType("text");
+
+                    b.Property<bool>("IsConnected")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsEnabled")
+                        .HasColumnType("boolean");
+
+                    b.Property<int?>("KeepAliveTimeout")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("NbRep")
+                        .HasColumnType("text");
+
+                    b.Property<string>("NwkSKey")
+                        .HasColumnType("text");
+
+                    b.Property<int>("PreferredWindow")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("RX1DROffset")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("RX2DataRate")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("RXDelay")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("ReportedRX1DROffset")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ReportedRX2DataRate")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ReportedRXDelay")
+                        .HasColumnType("text");
+
+                    b.Property<string>("SensorDecoder")
+                        .HasColumnType("text");
+
+                    b.Property<DateTime>("StatusUpdatedTime")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<bool?>("Supports32BitFCnt")
+                        .HasColumnType("boolean");
+
+                    b.Property<Dictionary<string, string>>("Tags")
+                        .IsRequired()
+                        .HasColumnType("jsonb");
+
+                    b.Property<string>("TxPower")
+                        .HasColumnType("text");
+
+                    b.Property<bool>("UseOTAA")
+                        .HasColumnType("boolean");
+
+                    b.Property<int>("Version")
+                        .HasColumnType("integer");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("LorawanDevices");
+                });
 #pragma warning restore 612, 618
         }
     }
diff --git a/src/AzureIoTHub.Portal.Infrastructure/ProductionConfigHandler.cs b/src/AzureIoTHub.Portal.Infrastructure/ProductionConfigHandler.cs
index b2e423dd2..04235edd2 100644
--- a/src/AzureIoTHub.Portal.Infrastructure/ProductionConfigHandler.cs
+++ b/src/AzureIoTHub.Portal.Infrastructure/ProductionConfigHandler.cs
@@ -16,6 +16,8 @@ internal ProductionConfigHandler(IConfiguration config)
 
         public override string PortalName => this.config[PortalNameKey];
 
+        public override int SyncDevicesJobRefreshIntervalInSeconds => this.config.GetValue(SyncDevicesJobRefreshIntervalKey, 300);
+
         public override int MetricExporterRefreshIntervalInSeconds => this.config.GetValue(MetricExporterRefreshIntervalKey, 30);
 
         public override int MetricLoaderRefreshIntervalInMinutes => this.config.GetValue(MetricLoaderRefreshIntervalKey, 10);
diff --git a/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs b/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs
index 4a7096d0e..b470a47c1 100644
--- a/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs
+++ b/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs
@@ -14,6 +14,7 @@ public class DeviceProfile : Profile
     {
         public DeviceProfile()
         {
+            _ = CreateMap<Device, Device>();
             _ = CreateMap<Twin, Device>()
                 .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.DeviceId))
                 .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Tags["deviceName"]))
@@ -23,6 +24,8 @@ public DeviceProfile()
                 .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)));
 
+
+            _ = CreateMap<LorawanDevice, LorawanDevice>();
             _ = CreateMap<Twin, LorawanDevice>()
                 .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.DeviceId))
                 .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Tags["deviceName"]))
diff --git a/src/AzureIoTHub.Portal/Server/Startup.cs b/src/AzureIoTHub.Portal/Server/Startup.cs
index 3c1aff26f..ef918cf72 100644
--- a/src/AzureIoTHub.Portal/Server/Startup.cs
+++ b/src/AzureIoTHub.Portal/Server/Startup.cs
@@ -322,8 +322,8 @@ Specify the authorization token got from your IDP as a header.
                         .WithIdentity($"{nameof(SyncDevicesJob)}")
                         .ForJob(nameof(SyncDevicesJob))
                         .WithSimpleSchedule(s => s
-                            //.WithIntervalInSeconds(configuration.MetricExporterRefreshIntervalInSeconds)
-                            .WithIntervalInSeconds(3000)
+                            .WithIntervalInSeconds(configuration.SyncDevicesJobRefreshIntervalInSeconds)
+                            //.WithIntervalInSeconds(3000)
                             .RepeatForever()));
             });
 
diff --git a/src/AzureIoTHubPortal.Domain/ConfigHandler.cs b/src/AzureIoTHubPortal.Domain/ConfigHandler.cs
index b4cc474bc..fe87e5d04 100644
--- a/src/AzureIoTHubPortal.Domain/ConfigHandler.cs
+++ b/src/AzureIoTHubPortal.Domain/ConfigHandler.cs
@@ -51,6 +51,8 @@ public abstract class ConfigHandler
 
         public abstract string PortalName { get; }
 
+        public abstract int SyncDevicesJobRefreshIntervalInSeconds { get; }
+
         public abstract int MetricExporterRefreshIntervalInSeconds { get; }
 
         public abstract int MetricLoaderRefreshIntervalInMinutes { get; }

From f9f05e8c18de9f4cf5bbce449ab657ce9319d683 Mon Sep 17 00:00:00 2001
From: ben salim <salim.benahben@cgi.com>
Date: Thu, 22 Sep 2022 14:49:19 +0200
Subject: [PATCH 07/11] fix unit test

---
 ...0_Add Device and LorawanDevice.Designer.cs} | 11 +++++------
 ...0922124300_Add Device and LorawanDevice.cs} |  5 ++---
 .../Migrations/PortalDbContextModelSnapshot.cs |  9 ++++-----
 .../PortalDbContext.cs                         | 18 ++++++++++++++++++
 .../UnitTests/Bases/BackendUnitTest.cs         |  1 +
 .../Entities/Device.cs                         |  2 --
 .../Entities/LorawanDevice.cs                  |  2 --
 7 files changed, 30 insertions(+), 18 deletions(-)
 rename src/AzureIoTHub.Portal.Infrastructure/Migrations/{20220922094312_Add Device and LorawanDevice.Designer.cs => 20220922124300_Add Device and LorawanDevice.Designer.cs} (97%)
 rename src/AzureIoTHub.Portal.Infrastructure/Migrations/{20220922094312_Add Device and LorawanDevice.cs => 20220922124300_Add Device and LorawanDevice.cs} (95%)

diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.Designer.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.Designer.cs
similarity index 97%
rename from src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.Designer.cs
rename to src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.Designer.cs
index 4ff1a743c..b2db44d9c 100644
--- a/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.Designer.cs	
+++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.Designer.cs	
@@ -1,6 +1,5 @@
 // <auto-generated />
 using System;
-using System.Collections.Generic;
 using AzureIoTHub.Portal.Infrastructure;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -13,7 +12,7 @@
 namespace AzureIoTHub.Portal.Infrastructure.Migrations
 {
     [DbContext(typeof(PortalDbContext))]
-    [Migration("20220922094312_Add Device and LorawanDevice")]
+    [Migration("20220922124300_Add Device and LorawanDevice")]
     partial class AddDeviceandLorawanDevice
     {
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -47,9 +46,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
                     b.Property<DateTime>("StatusUpdatedTime")
                         .HasColumnType("timestamp with time zone");
 
-                    b.Property<Dictionary<string, string>>("Tags")
+                    b.Property<string>("Tags")
                         .IsRequired()
-                        .HasColumnType("jsonb");
+                        .HasColumnType("text");
 
                     b.Property<int>("Version")
                         .HasColumnType("integer");
@@ -322,9 +321,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
                     b.Property<bool?>("Supports32BitFCnt")
                         .HasColumnType("boolean");
 
-                    b.Property<Dictionary<string, string>>("Tags")
+                    b.Property<string>("Tags")
                         .IsRequired()
-                        .HasColumnType("jsonb");
+                        .HasColumnType("text");
 
                     b.Property<string>("TxPower")
                         .HasColumnType("text");
diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.cs
similarity index 95%
rename from src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.cs
rename to src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.cs
index 8158e9abb..b12375418 100644
--- a/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922094312_Add Device and LorawanDevice.cs	
+++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.cs	
@@ -6,7 +6,6 @@
 namespace AzureIoTHub.Portal.Infrastructure.Migrations
 {
     using System;
-    using System.Collections.Generic;
     using Microsoft.EntityFrameworkCore.Migrations;
 
     public partial class AddDeviceandLorawanDevice : Migration
@@ -24,7 +23,7 @@ protected override void Up(MigrationBuilder migrationBuilder)
                     IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
                     Version = table.Column<int>(type: "integer", nullable: false),
                     StatusUpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
-                    Tags = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: false)
+                    Tags = table.Column<string>(type: "text", nullable: false)
                 },
                 constraints: table =>
                 {
@@ -42,7 +41,7 @@ protected override void Up(MigrationBuilder migrationBuilder)
                     IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
                     Version = table.Column<int>(type: "integer", nullable: false),
                     StatusUpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
-                    Tags = table.Column<Dictionary<string, string>>(type: "jsonb", nullable: false),
+                    Tags = table.Column<string>(type: "text", nullable: false),
                     UseOTAA = table.Column<bool>(type: "boolean", nullable: false),
                     AppKey = table.Column<string>(type: "text", nullable: true),
                     AppEUI = table.Column<string>(type: "text", nullable: true),
diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs
index 4b6f0f11f..ab7c30d91 100644
--- a/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs
+++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs
@@ -1,6 +1,5 @@
 // <auto-generated />
 using System;
-using System.Collections.Generic;
 using AzureIoTHub.Portal.Infrastructure;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -45,9 +44,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
                     b.Property<DateTime>("StatusUpdatedTime")
                         .HasColumnType("timestamp with time zone");
 
-                    b.Property<Dictionary<string, string>>("Tags")
+                    b.Property<string>("Tags")
                         .IsRequired()
-                        .HasColumnType("jsonb");
+                        .HasColumnType("text");
 
                     b.Property<int>("Version")
                         .HasColumnType("integer");
@@ -320,9 +319,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
                     b.Property<bool?>("Supports32BitFCnt")
                         .HasColumnType("boolean");
 
-                    b.Property<Dictionary<string, string>>("Tags")
+                    b.Property<string>("Tags")
                         .IsRequired()
-                        .HasColumnType("jsonb");
+                        .HasColumnType("text");
 
                     b.Property<string>("TxPower")
                         .HasColumnType("text");
diff --git a/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs b/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs
index ee82bd6ad..1eeefbcb6 100644
--- a/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs
+++ b/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs
@@ -9,6 +9,7 @@ namespace AzureIoTHub.Portal.Infrastructure
     using Microsoft.EntityFrameworkCore;
     using Microsoft.EntityFrameworkCore.Infrastructure;
     using Microsoft.Extensions.Logging;
+    using Newtonsoft.Json;
 
     public class PortalDbContext : DbContext
     {
@@ -27,5 +28,22 @@ public PortalDbContext(DbContextOptions<PortalDbContext> options)
             : base(options)
         {
         }
+
+        protected override void OnModelCreating(ModelBuilder modelBuilder)
+        {
+            base.OnModelCreating(modelBuilder);
+
+            _ = modelBuilder.Entity<Device>()
+                .Property(b => b.Tags)
+                .HasConversion(
+                    v => JsonConvert.SerializeObject(v),
+                    v => JsonConvert.DeserializeObject<Dictionary<string, string>>(v));
+
+            _ = modelBuilder.Entity<LorawanDevice>()
+                .Property(b => b.Tags)
+                .HasConversion(
+                    v => JsonConvert.SerializeObject(v),
+                    v => JsonConvert.DeserializeObject<Dictionary<string, string>>(v));
+        }
     }
 }
diff --git a/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs
index 0948fa51c..7f44d9343 100644
--- a/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs
+++ b/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs
@@ -53,6 +53,7 @@ public virtual void Setup()
                 mc.AddProfile(new DeviceTagProfile());
                 mc.AddProfile(new DeviceModelProfile());
                 mc.AddProfile(new DeviceModelCommandProfile());
+                mc.AddProfile(new DeviceProfile());
             });
             Mapper = mappingConfig.CreateMapper();
             _ = ServiceCollection.AddSingleton(Mapper);
diff --git a/src/AzureIoTHubPortal.Domain/Entities/Device.cs b/src/AzureIoTHubPortal.Domain/Entities/Device.cs
index 6c4368af3..97b7ccefd 100644
--- a/src/AzureIoTHubPortal.Domain/Entities/Device.cs
+++ b/src/AzureIoTHubPortal.Domain/Entities/Device.cs
@@ -3,7 +3,6 @@
 
 namespace AzureIoTHub.Portal.Domain.Entities
 {
-    using System.ComponentModel.DataAnnotations.Schema;
     using AzureIoTHub.Portal.Domain.Base;
 
     public class Device : EntityBase
@@ -38,7 +37,6 @@ public class Device : EntityBase
         /// <summary>
         /// List of custom device tags and their values.
         /// </summary>
-        [Column(TypeName = "jsonb")]
         public Dictionary<string, string> Tags { get; set; } = new();
     }
 }
diff --git a/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
index 7577240d5..f1af440d5 100644
--- a/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
+++ b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs
@@ -3,7 +3,6 @@
 
 namespace AzureIoTHub.Portal.Domain.Entities
 {
-    using System.ComponentModel.DataAnnotations.Schema;
     using AzureIoTHub.Portal.Domain.Base;
     using AzureIoTHub.Portal.Models.v10.LoRaWAN;
 
@@ -40,7 +39,6 @@ public class LorawanDevice : EntityBase
         /// <summary>
         /// List of custom device tags and their values.
         /// </summary>
-        [Column(TypeName = "jsonb")]
         public Dictionary<string, string> Tags { get; set; } = new();
 
         /// <summary>

From eb498eeb438c35c4106cfffff1670c593993a2f6 Mon Sep 17 00:00:00 2001
From: ben salim <salim.benahben@cgi.com>
Date: Thu, 22 Sep 2022 14:55:24 +0200
Subject: [PATCH 08/11] delete useless code

---
 src/AzureIoTHub.Portal/Server/Startup.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/AzureIoTHub.Portal/Server/Startup.cs b/src/AzureIoTHub.Portal/Server/Startup.cs
index ef918cf72..1ae070467 100644
--- a/src/AzureIoTHub.Portal/Server/Startup.cs
+++ b/src/AzureIoTHub.Portal/Server/Startup.cs
@@ -323,7 +323,6 @@ Specify the authorization token got from your IDP as a header.
                         .ForJob(nameof(SyncDevicesJob))
                         .WithSimpleSchedule(s => s
                             .WithIntervalInSeconds(configuration.SyncDevicesJobRefreshIntervalInSeconds)
-                            //.WithIntervalInSeconds(3000)
                             .RepeatForever()));
             });
 

From cfde81bb43c7c8baddfd2aa4fb7b1ffb33a311b2 Mon Sep 17 00:00:00 2001
From: ben salim <salim.benahben@cgi.com>
Date: Thu, 22 Sep 2022 15:37:44 +0200
Subject: [PATCH 09/11] add unit test device repository

---
 .../Repositories/DeviceRepositoryTest.cs      | 43 +++++++++++++++++++
 .../LorawanDeviceRepositoryTest.cs            | 43 +++++++++++++++++++
 2 files changed, 86 insertions(+)
 create mode 100644 src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/DeviceRepositoryTest.cs
 create mode 100644 src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/LorawanDeviceRepositoryTest.cs

diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/DeviceRepositoryTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/DeviceRepositoryTest.cs
new file mode 100644
index 000000000..412dfff07
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/DeviceRepositoryTest.cs
@@ -0,0 +1,43 @@
+// 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 DeviceRepositoryTest : BackendUnitTest
+    {
+        private DeviceRepository deviceRepository;
+
+        public override void Setup()
+        {
+            base.Setup();
+
+            this.deviceRepository = new DeviceRepository(DbContext);
+        }
+
+        [Test]
+        public async Task GetAllShouldReturnExpectedDevices()
+        {
+            // Arrange
+            var expectedDevices = Fixture.CreateMany<Device>(5).ToList();
+
+            await DbContext.AddRangeAsync(expectedDevices);
+
+            _ = await DbContext.SaveChangesAsync();
+
+            //Act
+            var result = this.deviceRepository.GetAll().ToList();
+
+            // Assert
+            _ = result.Should().BeEquivalentTo(expectedDevices);
+        }
+    }
+}
diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/LorawanDeviceRepositoryTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/LorawanDeviceRepositoryTest.cs
new file mode 100644
index 000000000..71aaecdae
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/LorawanDeviceRepositoryTest.cs
@@ -0,0 +1,43 @@
+// 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 AutoFixture;
+    using System.Threading.Tasks;
+    using AzureIoTHub.Portal.Infrastructure.Repositories;
+    using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases;
+    using NUnit.Framework;
+    using AzureIoTHub.Portal.Domain.Entities;
+    using System.Linq;
+    using FluentAssertions;
+
+    public class LorawanDeviceRepositoryTest : BackendUnitTest
+    {
+        private LorawanDeviceRepository lorawanDeviceRepository;
+
+        public override void Setup()
+        {
+            base.Setup();
+
+            this.lorawanDeviceRepository = new LorawanDeviceRepository(DbContext);
+        }
+
+        [Test]
+        public async Task GetAllShouldReturnExpectedDevices()
+        {
+            // Arrange
+            var expectedDevices = Fixture.CreateMany<LorawanDevice>(5).ToList();
+
+            await DbContext.AddRangeAsync(expectedDevices);
+
+            _ = await DbContext.SaveChangesAsync();
+
+            //Act
+            var result = this.lorawanDeviceRepository.GetAll().ToList();
+
+            // Assert
+            _ = result.Should().BeEquivalentTo(expectedDevices);
+        }
+    }
+}

From 1dbf16fb8cb500f4d0a622b4354d4dbe691e30a2 Mon Sep 17 00:00:00 2001
From: ben salim <salim.benahben@cgi.com>
Date: Thu, 22 Sep 2022 16:04:37 +0200
Subject: [PATCH 10/11] add test on configHandler production/development

---
 .../Infrastructure/DevelopmentConfigHandlerTests.cs  | 10 ++++++++++
 .../Infrastructure/ProductionConfigHandlerTests.cs   | 12 ++++++++++--
 2 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs
index 7d389c50b..7886fbce6 100644
--- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs
+++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs
@@ -159,6 +159,16 @@ public void SettingsShouldGetBoolFromAppSettings(string configKey, string config
             this.mockRepository.VerifyAll();
         }
 
+        [Test]
+        public void SyncDevicesJobRefreshIntervalInSecondsConfigMustHaveDefaultValue()
+        {
+            // Arrange
+            var developmentConfigHandler = new DevelopmentConfigHandler(new ConfigurationManager());
+
+            // Assert
+            _ = developmentConfigHandler.SyncDevicesJobRefreshIntervalInSeconds.Should().Be(300);
+        }
+
         [Test]
         public void MetricExporterRefreshIntervalInSecondsConfigMustHaveDefaultValue()
         {
diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionConfigHandlerTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionConfigHandlerTests.cs
index 5b9c09e1f..c52d8884d 100644
--- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionConfigHandlerTests.cs
+++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionConfigHandlerTests.cs
@@ -3,10 +3,8 @@
 
 namespace AzureIoTHub.Portal.Tests.Unit.Infrastructure
 {
-    using AzureIoTHub.Portal.Server;
     using System;
     using System.Globalization;
-    using System.Reflection;
     using FluentAssertions;
     using Microsoft.Extensions.Configuration;
     using Moq;
@@ -185,6 +183,16 @@ public void SettingsShouldGetBoolFromAppSettings(string configKey, string config
             this.mockRepository.VerifyAll();
         }
 
+        [Test]
+        public void SyncDevicesJobRefreshIntervalInSecondsConfigMustHaveDefaultValue()
+        {
+            // Arrange
+            var productionConfigHandler = new ProductionConfigHandler(new ConfigurationManager());
+
+            // Assert
+            _ = productionConfigHandler.SyncDevicesJobRefreshIntervalInSeconds.Should().Be(300);
+        }
+
         [Test]
         public void MetricExporterRefreshIntervalInSecondsConfigMustHaveDefaultValue()
         {

From 77b5759b5eb51da3bd2c81d23c1aa79e81b64cd3 Mon Sep 17 00:00:00 2001
From: ben salim <salim.benahben@cgi.com>
Date: Fri, 23 Sep 2022 15:12:03 +0200
Subject: [PATCH 11/11] add test for syncDeviceJob

---
 .../Server/Jobs/SyncDeviceJobTest.cs          | 113 ++++++++++++++++++
 .../Server/Jobs/SyncDevicesJob.cs             |  67 +++++++----
 2 files changed, 155 insertions(+), 25 deletions(-)
 create mode 100644 src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncDeviceJobTest.cs

diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncDeviceJobTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncDeviceJobTest.cs
new file mode 100644
index 000000000..4874e26b5
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncDeviceJobTest.cs
@@ -0,0 +1,113 @@
+// 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;
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Threading.Tasks;
+    using AutoMapper;
+    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 Microsoft.Azure.Devices.Shared;
+    using Microsoft.Extensions.Logging;
+    using Moq;
+    using NUnit.Framework;
+    using Quartz;
+
+    public class SyncDeviceJobTest : IDisposable
+    {
+        private MockRepository mockRepository;
+        private SyncDevicesJob syncDevicesJob;
+
+        private Mock<ILogger<SyncDevicesJob>> mockLogger;
+        private Mock<IDeviceService> mockDeviceService;
+        private Mock<IDeviceModelRepository> mockDeviceModelRepository;
+        private Mock<ILorawanDeviceRepository> mockLorawanDeviceRepository;
+        private Mock<IDeviceRepository> mockDeviceRepository;
+        private Mock<IUnitOfWork> mockUnitOfWork;
+        private Mock<IMapper> mockMapper;
+
+        [SetUp]
+        public void SetUp()
+        {
+            this.mockRepository = new MockRepository(MockBehavior.Strict);
+
+            this.mockLogger = this.mockRepository.Create<ILogger<SyncDevicesJob>>();
+            this.mockDeviceService = this.mockRepository.Create<IDeviceService>();
+            this.mockDeviceModelRepository = this.mockRepository.Create<IDeviceModelRepository>();
+            this.mockLorawanDeviceRepository = this.mockRepository.Create<ILorawanDeviceRepository>();
+            this.mockDeviceRepository = this.mockRepository.Create<IDeviceRepository>();
+            this.mockUnitOfWork = this.mockRepository.Create<IUnitOfWork>();
+            this.mockMapper = this.mockRepository.Create<IMapper>();
+
+            this.syncDevicesJob =
+                new SyncDevicesJob(
+                this.mockDeviceService.Object,
+                this.mockDeviceModelRepository.Object,
+                this.mockLorawanDeviceRepository.Object,
+                this.mockDeviceRepository.Object,
+                this.mockMapper.Object,
+                this.mockUnitOfWork.Object,
+                this.mockLogger.Object);
+        }
+
+        [Test]
+        public async Task ExecuteShould()
+        {
+            // Arrange
+            var mockJobExecutionContext = this.mockRepository.Create<IJobExecutionContext>();
+            var twinCollection = new TwinCollection();
+
+            _ = this.mockDeviceService
+                .Setup(x => x.GetAllDevice(
+                    It.IsAny<string>(),
+                    It.IsAny<string>(),
+                    It.Is<string>(x => x == "LoRa Concentrator"),
+                    It.IsAny<string>(),
+                    It.IsAny<bool?>(),
+                    It.IsAny<bool?>(),
+                    It.IsAny<Dictionary<string, string>>(),
+                    It.Is<int>(x => x == 100)))
+                .ReturnsAsync(new PaginationResult<Twin>
+                {
+                    Items = Enumerable.Range(0, 100).Select(x => new Twin
+                    {
+                        DeviceId = FormattableString.Invariant($"{x}"),
+                        Tags = twinCollection
+                    }),
+                    TotalItems = 1000,
+                    NextPage = Guid.NewGuid().ToString()
+                });
+
+
+            _ = this.mockDeviceModelRepository
+                .Setup(x => x.GetByIdAsync(It.IsAny<string>()))
+                .ReturnsAsync(new Portal.Domain.Entities.DeviceModel()
+                {
+                    Id = Guid.NewGuid().ToString(),
+                    SupportLoRaFeatures = true
+                });
+
+            // Act
+            await this.syncDevicesJob.Execute(mockJobExecutionContext.Object);
+
+            // Assert
+            this.mockRepository.VerifyAll();
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+        }
+    }
+}
diff --git a/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs b/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
index b6c6fff0d..f0dbad0c4 100644
--- a/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
+++ b/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
@@ -11,6 +11,7 @@ namespace AzureIoTHub.Portal.Server.Jobs
     using AzureIoTHub.Portal.Domain.Repositories;
     using AzureIoTHub.Portal.Server.Services;
     using Microsoft.Azure.Devices.Shared;
+    using Microsoft.Extensions.Logging;
     using Quartz;
 
     [DisallowConcurrentExecution]
@@ -22,13 +23,15 @@ public class SyncDevicesJob : IJob
         private readonly IDeviceRepository deviceRepository;
         private readonly IMapper mapper;
         private readonly IUnitOfWork unitOfWork;
+        private readonly ILogger<SyncDevicesJob> logger;
 
         public SyncDevicesJob(IDeviceService deviceService,
             IDeviceModelRepository deviceModelRepository,
             ILorawanDeviceRepository lorawanDeviceRepository,
             IDeviceRepository deviceRepository,
             IMapper mapper,
-            IUnitOfWork unitOfWork)
+            IUnitOfWork unitOfWork,
+            ILogger<SyncDevicesJob> logger)
         {
             this.deviceService = deviceService;
             this.deviceModelRepository = deviceModelRepository;
@@ -36,23 +39,12 @@ public SyncDevicesJob(IDeviceService deviceService,
             this.deviceRepository = deviceRepository;
             this.mapper = mapper;
             this.unitOfWork = unitOfWork;
+            this.logger = logger;
         }
 
         public async Task Execute(IJobExecutionContext context)
         {
-            var twins = new List<Twin>();
-            var continuationToken = string.Empty;
-
-            int totalTwinDevices;
-            do
-            {
-                var result = await this.deviceService.GetAllDevice(continuationToken: continuationToken,excludeDeviceType: "LoRa Concentrator", pageSize: 100);
-                twins.AddRange(result.Items);
-
-                totalTwinDevices = result.TotalItems;
-                continuationToken = result.NextPage;
-
-            } while (totalTwinDevices > twins.Count);
+            var twins = await GetAllDeviceTwin();
 
             foreach (var twin in twins)
             {
@@ -65,21 +57,28 @@ public async Task Execute(IJobExecutionContext context)
 
                 if (model.SupportLoRaFeatures)
                 {
-                    var device = this.mapper.Map<LorawanDevice>(twin);
-
-                    var lorawanDeviceEntity = await this.lorawanDeviceRepository.GetByIdAsync(device.Id);
-                    if (lorawanDeviceEntity == null)
+                    try
                     {
-                        await this.lorawanDeviceRepository.InsertAsync(device);
-                    }
-                    else
-                    {
-                        if (lorawanDeviceEntity.Version < device.Version)
+                        var device = this.mapper.Map<LorawanDevice>(twin);
+
+                        var lorawanDeviceEntity = await this.lorawanDeviceRepository.GetByIdAsync(device.Id);
+                        if (lorawanDeviceEntity == null)
+                        {
+                            await this.lorawanDeviceRepository.InsertAsync(device);
+                        }
+                        else
                         {
-                            _ = this.mapper.Map(device, lorawanDeviceEntity);
-                            this.lorawanDeviceRepository.Update(lorawanDeviceEntity);
+                            if (lorawanDeviceEntity.Version < device.Version)
+                            {
+                                _ = this.mapper.Map(device, lorawanDeviceEntity);
+                                this.lorawanDeviceRepository.Update(lorawanDeviceEntity);
+                            }
                         }
                     }
+                    catch (System.Exception)
+                    {
+                        this.logger.LogWarning($"Error while attenpting to insert LoRa device {twin.DeviceId}.");
+                    }
                 }
                 else
                 {
@@ -103,7 +102,25 @@ public async Task Execute(IJobExecutionContext context)
 
             // save entity
             await this.unitOfWork.SaveAsync();
+        }
+
+        private async Task<List<Twin>> GetAllDeviceTwin()
+        {
+            var twins = new List<Twin>();
+            var continuationToken = string.Empty;
+
+            int totalTwinDevices;
+            do
+            {
+                var result = await this.deviceService.GetAllDevice(continuationToken: continuationToken,excludeDeviceType: "LoRa Concentrator", pageSize: 100);
+                twins.AddRange(result.Items);
+
+                totalTwinDevices = result.TotalItems;
+                continuationToken = result.NextPage;
+
+            } while (totalTwinDevices > twins.Count);
 
+            return twins;
         }
     }
 }