- Fetch ticket data using ManageEngine’s REST API.
- Store it in SQL Server using Table-Valued Parameters (TVP) for efficiency.
- Build a dashboard to visualize ticket metrics like total, open, assigned, and closed tickets.
- Implement secure credential management and robust error handling.
- ManageEngine ServiceDesk Plus self-hosted instance (URL: https://helpdesk.edison-bd.com:8989/)
- ASP.NET MVC 5 project setup in Visual Studio
- SQL Server database
- Postman (for testing API endpoints)
- NuGet Packages:
- Newtonsoft.Json (for JSON parsing)
- System.Data.SqlClient (for SQL Server operations)
- Microsoft.Extensions.Configuration (for secure configuration)
- Set up ManageEngine API Access
- Generate an API key for a technician with appropriate permissions.
- Create SQL Server Database and Stored Procedure
- Design a table for tickets and a stored procedure using TVP.
- Develop ASP.NET MVC 5 Application
- Fetch ticket data via REST API.
- Store data in a DataTable and insert into SQL Server.
- Build a dashboard for ticket analytics.
- Implement Error Handling and Credential Management
- Use secure configuration and logging.
- Write a Blog Post
- Document the process with personal branding.
- Log in to ManageEngine ServiceDesk Plus:
- Navigate to Admin -> Technicians.
- Select or create a technician with login permissions and appropriate roles (e.g., access to view tickets).
- Generate an API key for this technician under their profile settings.
- Test the API:
- Use Postman to verify the API endpoint: https://helpdesk.edison-bd.com:8989/api/v3/requests.
- Send a GET request with the following headers:
- TECHNICIAN_KEY: Your API key
- Content-Type: application/json
- Example request to fetch tickets by date:http
GET /api/v3/requests?input_data={"list_info":{"row_count":100,"start_index":1,"search_criteria":[{"field":"created_time","condition":"greater than","value":"2025-01-01 00:00:00"}]}} - Verify the response contains ticket data with fields like id, subject, created_time, etc.
CREATE TABLE Tickets (
TicketId BIGINT PRIMARY KEY,
Subject NVARCHAR(255),
CreatedTime DATETIME,
GroupName NVARCHAR(100),
Mode NVARCHAR(50),
RequestType NVARCHAR(50),
Level NVARCHAR(50),
ServiceCategory NVARCHAR(100),
Department NVARCHAR(100),
Urgency NVARCHAR(50),
Category NVARCHAR(100),
Subcategory NVARCHAR(100),
Site NVARCHAR(100),
Status NVARCHAR(50),
Requestor NVARCHAR(100),
Technician NVARCHAR(100),
LastUpdated DATETIME,
InsertedDate DATETIME DEFAULT GETDATE()
);CREATE TYPE TicketTVP AS TABLE (
TicketId BIGINT,
Subject NVARCHAR(255),
CreatedTime DATETIME,
GroupName NVARCHAR(100),
Mode NVARCHAR(50),
RequestType NVARCHAR(50),
Level NVARCHAR(50),
ServiceCategory NVARCHAR(100),
Department NVARCHAR(100),
Urgency NVARCHAR(50),
Category NVARCHAR(100),
Subcategory NVARCHAR(100),
Site NVARCHAR(100),
Status NVARCHAR(50),
Requestor NVARCHAR(100),
Technician NVARCHAR(100),
LastUpdated DATETIME
);CREATE PROCEDURE sp_InsertTickets
@Tickets TicketTVP READONLY
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- Merge to handle inserts and updates
MERGE INTO Tickets AS target
USING @Tickets AS source
ON target.TicketId = source.TicketId
WHEN MATCHED THEN
UPDATE SET
Subject = source.Subject,
CreatedTime = source.CreatedTime,
GroupName = source.GroupName,
Mode = source.Mode,
RequestType = source.RequestType,
Level = source.Level,
ServiceCategory = source.ServiceCategory,
Department = source.Department,
Urgency = source.Urgency,
Category = source.Category,
Subcategory = source.Subcategory,
Site = source.Site,
Status = source.Status,
Requestor = source.Requestor,
Technician = source.Technician,
LastUpdated = source.LastUpdated,
InsertedDate = GETDATE()
WHEN NOT MATCHED THEN
INSERT (
TicketId, Subject, CreatedTime, GroupName, Mode, RequestType, Level,
ServiceCategory, Department, Urgency, Category, Subcategory, Site,
Status, Requestor, Technician, LastUpdated, InsertedDate
)
VALUES (
source.TicketId, source.Subject, source.CreatedTime, source.GroupName,
source.Mode, source.RequestType, source.Level, source.ServiceCategory,
source.Department, source.Urgency, source.Category, source.Subcategory,
source.Site, source.Status, source.Requestor, source.Technician,
source.LastUpdated, GETDATE()
);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
DECLARE @ErrorSeverity INT = ERROR_SEVERITY();
DECLARE @ErrorState INT = ERROR_STATE();
-- Log error (assuming a logging table exists)
INSERT INTO ErrorLog (ErrorMessage, ErrorDate)
VALUES (@ErrorMessage, GETDATE());
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState);
END CATCH;
END;- Create a new ASP.NET MVC 5 project in Visual Studio.
- Install required NuGet packages:bash
Install-Package Newtonsoft.Json Install-Package Microsoft.Extensions.Configuration Install-Package Microsoft.Extensions.Configuration.Json - Add an appsettings.json file for configuration:json
{ "ManageEngine": { "ApiUrl": "https://helpdesk.edison-bd.com:8989/api/v3/requests", "TechnicianKey": "YOUR_API_KEY_HERE" }, "ConnectionStrings": { "DefaultConnection": "Server=your_server;Database=your_db;Trusted_Connection=True;" } } - Configure Startup.cs to load appsettings.json:csharp
using Microsoft.Extensions.Configuration; using System.IO; public class Startup { public static IConfiguration Configuration { get; private set; } public Startup() { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); Configuration = builder.Build(); } }
using System;
namespace ManageEngineIntegration.Models
{
public class Ticket
{
public long TicketId { get; set; }
public string Subject { get; set; }
public DateTime CreatedTime { get; set; }
public string GroupName { get; set; }
public string Mode { get; set; }
public string RequestType { get; set; }
public string Level { get; set; }
public string ServiceCategory { get; set; }
public string Department { get; set; }
public string Urgency { get; set; }
public string Category { get; set; }
public string Subcategory { get; set; }
public string Site { get; set; }
public string Status { get; set; }
public string Requestor { get; set; }
public string Technician { get; set; }
public DateTime LastUpdated { get; set; }
}
}using System.Collections.Generic;
namespace ManageEngineIntegration.Models
{
public class TicketResponse
{
public List<TicketData> Requests { get; set; }
}
public class TicketData
{
public long Id { get; set; }
public string Subject { get; set; }
public DateTimeValue Created_time { get; set; }
public NameId Group { get; set; }
public NameId Mode { get; set; }
public NameId Request_type { get; set; }
public NameId Level { get; set; }
public NameId Service_category { get; set; }
public NameId Department { get; set; }
public NameId Urgency { get; set; }
public NameId Category { get; set; }
public NameId Subcategory { get; set; }
public NameId Site { get; set; }
public NameId Status { get; set; }
public NameId Requester { get; set; }
public NameId Technician { get; set; }
public DateTimeValue Last_updated_time { get; set; }
}
public class NameId
{
public string Name { get; set; }
public long? Id { get; set; }
}
public class DateTimeValue
{
public string Display_value { get; set; }
public long Value { get; set; }
}
}using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using System;
using System.Data;
using System.Net.Http;
using System.Threading.Tasks;
using ManageEngineIntegration.Models;
using System.Collections.Generic;
namespace ManageEngineIntegration.Services
{
public class ManageEngineService
{
private readonly IConfiguration _configuration;
private readonly HttpClient _httpClient;
public ManageEngineService(IConfiguration configuration)
{
_configuration = configuration;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("TECHNICIAN_KEY", _configuration["ManageEngine:TechnicianKey"]);
}
public async Task<DataTable> GetTicketsByDateAsync(DateTime startDate, DateTime endDate)
{
try
{
var inputData = new
{
list_info = new
{
row_count = 100,
start_index = 1,
search_criteria = new[]
{
new { field = "created_time", condition = "greater than", value = startDate.ToString("yyyy-MM-dd HH:mm:ss") },
new { field = "created_time", condition = "less than", value = endDate.ToString("yyyy-MM-dd HH:mm:ss") }
}
}
};
var content = new StringContent(JsonConvert.SerializeObject(inputData), System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(_configuration["ManageEngine:ApiUrl"], content);
if (!response.IsSuccessStatusCode)
{
throw new Exception($"API call failed with status code: {response.StatusCode}");
}
var json = await response.Content.ReadAsStringAsync();
var ticketResponse = JsonConvert.DeserializeObject<TicketResponse>(json);
return ConvertToDataTable(ticketResponse.Requests);
}
catch (Exception ex)
{
// Log error (use a logging framework like Serilog in production)
Console.WriteLine($"Error fetching tickets: {ex.Message}");
throw;
}
}
public async Task<Ticket> GetTicketByIdAsync(long ticketId)
{
try
{
var url = $"{_configuration["ManageEngine:ApiUrl"]}/{ticketId}";
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
throw new Exception($"API call failed with status code: {response.StatusCode}");
}
var json = await response.Content.ReadAsStringAsync();
var ticketData = JsonConvert.DeserializeObject<TicketData>(json);
return new Ticket
{
TicketId = ticketData.Id,
Subject = ticketData.Subject,
CreatedTime = DateTimeOffset.FromUnixTimeMilliseconds(ticketData.Created_time.Value).DateTime,
GroupName = ticketData.Group?.Name,
Mode = ticketData.Mode?.Name,
RequestType = ticketData.Request_type?.Name,
Level = ticketData.Level?.Name,
ServiceCategory = ticketData.Service_category?.Name,
Department = ticketData.Department?.Name,
Urgency = ticketData.Urgency?.Name,
Category = ticketData.Category?.Name,
Subcategory = ticketData.Subcategory?.Name,
Site = ticketData.Site?.Name,
Status = ticketData.Status?.Name,
Requestor = ticketData.Requester?.Name,
Technician = ticketData.Technician?.Name,
LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(ticketData.Last_updated_time.Value).DateTime
};
}
catch (Exception ex)
{
Console.WriteLine($"Error fetching ticket {ticketId}: {ex.Message}");
throw;
}
}
public async Task<DataTable> GetTicketsByRequestorOrTechnicianAsync(string requestor = null, string technician = null)
{
try
{
var criteria = new List<object>();
if (!string.IsNullOrEmpty(requestor))
{
criteria.Add(new { field = "requester.name", condition = "is", value = requestor });
}
if (!string.IsNullOrEmpty(technician))
{
criteria.Add(new { field = "technician.name", condition = "is", value = technician });
}
var inputData = new
{
list_info = new
{
row_count = 100,
start_index = 1,
search_criteria = criteria
}
};
var content = new StringContent(JsonConvert.SerializeObject(inputData), System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(_configuration["ManageEngine:ApiUrl"], content);
if (!response.IsSuccessStatusCode)
{
throw new Exception($"API call failed with status code: {response.StatusCode}");
}
var json = await response.Content.ReadAsStringAsync();
var ticketResponse = JsonConvert.DeserializeObject<TicketResponse>(json);
return ConvertToDataTable(ticketResponse.Requests);
}
catch (Exception ex)
{
Console.WriteLine($"Error fetching tickets: {ex.Message}");
throw;
}
}
private DataTable ConvertToDataTable(List<TicketData> tickets)
{
var dt = new DataTable();
dt.Columns.Add("TicketId", typeof(long));
dt.Columns.Add("Subject", typeof(string));
dt.Columns.Add("CreatedTime", typeof(DateTime));
dt.Columns.Add("GroupName", typeof(string));
dt.Columns.Add("Mode", typeof(string));
dt.Columns.Add("RequestType", typeof(string));
dt.Columns.Add("Level", typeof(string));
dt.Columns.Add("ServiceCategory", typeof(string));
dt.Columns.Add("Department", typeof(string));
dt.Columns.Add("Urgency", typeof(string));
dt.Columns.Add("Category", typeof(string));
dt.Columns.Add("Subcategory", typeof(string));
dt.Columns.Add("Site", typeof(string));
dt.Columns.Add("Status", typeof(string));
dt.Columns.Add("Requestor", typeof(string));
dt.Columns.Add("Technician", typeof(string));
dt.Columns.Add("LastUpdated", typeof(DateTime));
foreach (var ticket in tickets)
{
dt.Rows.Add(
ticket.Id,
ticket.Subject,
DateTimeOffset.FromUnixTimeMilliseconds(ticket.Created_time.Value).DateTime,
ticket.Group?.Name,
ticket.Mode?.Name,
ticket.Request_type?.Name,
ticket.Level?.Name,
ticket.Service_category?.Name,
ticket.Department?.Name,
ticket.Urgency?.Name,
ticket.Category?.Name,
ticket.Subcategory?.Name,
ticket.Site?.Name,
ticket.Status?.Name,
ticket.Requester?.Name,
ticket.Technician?.Name,
DateTimeOffset.FromUnixTimeMilliseconds(ticket.Last_updated_time.Value).DateTime
);
}
return dt;
}
}
}using Microsoft.Extensions.Configuration;
using System;
using System.Data;
using System.Data.SqlClient;
namespace ManageEngineIntegration.Repositories
{
public class TicketRepository
{
private readonly string _connectionString;
public TicketRepository(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection");
}
public void InsertTickets(DataTable tickets)
{
using (var connection = new SqlConnection(_connectionString))
{
try
{
connection.Open();
using (var command = new SqlCommand("sp_InsertTickets", connection))
{
command.CommandType = CommandType.StoredProcedure;
var parameter = command.Parameters.AddWithValue("@Tickets", tickets);
parameter.SqlDbType = SqlDbType.Structured;
parameter.TypeName = "TicketTVP";
command.ExecuteNonQuery();
}
}
catch (SqlException ex)
{
// Log error (use a logging framework in production)
Console.WriteLine($"Database error: {ex.Message}");
throw;
}
finally
{
connection.Close();
}
}
}
}
}using Microsoft.AspNet.Mvc;
using ManageEngineIntegration.Models;
using ManageEngineIntegration.Services;
using ManageEngineIntegration.Repositories;
using System;
using System.Threading.Tasks;
namespace ManageEngineIntegration.Controllers
{
public class TicketController : Controller
{
private readonly ManageEngineService _manageEngineService;
private readonly TicketRepository _ticketRepository;
public TicketController(ManageEngineService manageEngineService, TicketRepository ticketRepository)
{
_manageEngineService = manageEngineService;
_ticketRepository = ticketRepository;
}
public IActionResult Index()
{
return View();
}
[HttpPost]
public async Task<IActionResult> FetchTicketsByDate(DateTime startDate, DateTime endDate)
{
try
{
var tickets = await _manageEngineService.GetTicketsByDateAsync(startDate, endDate);
_ticketRepository.InsertTickets(tickets);
return Json(new { success = true, message = "Tickets fetched and stored successfully." });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
[HttpGet]
public async Task<IActionResult> GetTicketDetails(long ticketId)
{
try
{
var ticket = await _manageEngineService.GetTicketByIdAsync(ticketId);
return Json(ticket);
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
[HttpGet]
public async Task<IActionResult> GetTicketsByRequestorOrTechnician(string requestor = null, string technician = null)
{
try
{
var tickets = await _manageEngineService.GetTicketsByRequestorOrTechnicianAsync(requestor, technician);
return Json(tickets);
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
public IActionResult Dashboard()
{
return View();
}
}
}@{
ViewBag.Title = "Ticket Management";
}
<h2>Ticket Management</h2>
<form id="fetchTicketsForm">
<div class="form-group">
<label>Start Date</label>
<input type="date" id="startDate" class="form-control" />
</div>
<div class="form-group">
<label>End Date</label>
<input type="date" id="endDate" class="form-control" />
</div>
<button type="submit" class="btn btn-primary">Fetch Tickets</button>
</form>
<script>
$("#fetchTicketsForm").submit(function (e) {
e.preventDefault();
$.ajax({
url: '@Url.Action("FetchTicketsByDate")',
type: 'POST',
data: { startDate: $("#startDate").val(), endDate: $("#endDate").val() },
success: function (response) {
alert(response.message);
},
error: function () {
alert("An error occurred.");
}
});
});
</script>@{
ViewBag.Title = "Ticket Dashboard";
}
<h2>Ticket Dashboard</h2>
<div class="row">
<div class="col-md-6">
<h3>Date-wise Statistics</h3>
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Total Tickets</th>
<th>Open</th>
<th>Assigned</th>
<th>Closed</th>
</tr>
</thead>
<tbody id="dateStats"></tbody>
</table>
</div>
<div class="col-md-6">
<h3>Technician-wise Statistics</h3>
<table class="table">
<thead>
<tr>
<th>Technician</th>
<th>Total Tickets</th>
<th>Open</th>
<th>Assigned</th>
<th>Closed</th>
</tr>
</thead>
<tbody id="technicianStats"></tbody>
</table>
</div>
</div>
<script>
$(document).ready(function () {
// Fetch date-wise stats
$.get('/Ticket/GetTicketsByDate', function (data) {
let html = '';
// Process data and populate table (implement based on SQL query results)
$("#dateStats").html(html);
});
// Fetch technician-wise stats
$.get('/Ticket/GetTicketsByRequestorOrTechnician?technician=', function (data) {
let html = '';
// Process data and populate table (implement based on SQL query results)
$("#technicianStats").html(html);
});
});
</script>public DataTable GetDashboardStats(string type, DateTime? startDate = null, DateTime? endDate = null)
{
using (var connection = new SqlConnection(_connectionString))
{
try
{
connection.Open();
var dt = new DataTable();
using (var command = new SqlCommand("sp_GetDashboardStats", connection))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddWithValue("@Type", type);
if (startDate.HasValue)
command.Parameters.AddWithValue("@StartDate", startDate);
if (endDate.HasValue)
command.Parameters.AddWithValue("@EndDate", endDate);
using (var adapter = new SqlDataAdapter(command))
{
adapter.Fill(dt);
}
}
return dt;
}
catch (SqlException ex)
{
Console.WriteLine($"Database error: {ex.Message}");
throw;
}
finally
{
connection.Close();
}
}
}CREATE PROCEDURE sp_GetDashboardStats
@Type NVARCHAR(50),
@StartDate DATETIME = NULL,
@EndDate DATETIME = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @Type = 'DateWise'
BEGIN
SELECT
CONVERT(DATE, CreatedTime) AS TicketDate,
COUNT(*) AS TotalTickets,
SUM(CASE WHEN Status = 'Open' THEN 1 ELSE 0 END) AS OpenTickets,
SUM(CASE WHEN Technician IS NOT NULL THEN 1 ELSE 0 END) AS AssignedTickets,
SUM(CASE WHEN Status = 'Closed' THEN 1 ELSE 0 END) AS ClosedTickets
FROM Tickets
WHERE (@StartDate IS NULL OR CreatedTime >= @StartDate)
AND (@EndDate IS NULL OR CreatedTime <= @EndDate)
GROUP BY CONVERT(DATE, CreatedTime);
END
ELSE IF @Type = 'TechnicianWise'
BEGIN
SELECT
Technician,
COUNT(*) AS TotalTickets,
SUM(CASE WHEN Status = 'Open' THEN 1 ELSE 0 END) AS OpenTickets,
SUM(CASE WHEN Technician IS NOT NULL THEN 1 ELSE 0 END) AS AssignedTickets,
SUM(CASE WHEN Status = 'Closed' THEN 1 ELSE 0 END) AS ClosedTickets
FROM Tickets
WHERE (@StartDate IS NULL OR CreatedTime >= @StartDate)
AND (@EndDate IS NULL OR CreatedTime <= @EndDate)
GROUP BY Technician;
END
END;- API Calls: Use try-catch blocks to handle HTTP errors and log them (e.g., using Serilog in production).
- Database Operations: Use transactions and try-catch blocks in stored procedures to ensure data integrity.
- Logging: Create an ErrorLog table and log errors in both C# and SQL.
CREATE TABLE ErrorLog (
Id INT IDENTITY(1,1) PRIMARY KEY,
ErrorMessage NVARCHAR(MAX),
ErrorDate DATETIME DEFAULT GETDATE()
);- Store the API key in appsettings.json and access it via IConfiguration.
- For production, use Azure Key Vault or AWS Secrets Manager to store sensitive data.
- Ensure the API key is not hardcoded or exposed in logs.
- Testing: Test the API calls with Postman before integrating.
- Security: Use HTTPS for all API calls and secure the API key.
- Scalability: For large ticket volumes, implement pagination in the API calls.
- Logging: Integrate a logging framework like Serilog for production.
- Source Code: Host the complete code in a GitHub repository for easy access.
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam