
convert-webmethod-to-api
by AiLA-X-1
SKILL.md
name: convert-webmethod-to-api description: Convert ASP.NET 4.8 WebMethods from LargoV3 to .NET Core 8 API endpoints in AiLA_WebAPI. Use when asked to migrate, convert, or port WebMethods, ASPX page methods, or ASMX service methods to the new .NET Core API project. Creates DTOs, Repository interfaces and implementations, Controllers with proper ApiResponse wrappers, and registers dependencies.
Convert ASP.NET 4.8 WebMethod to .NET Core API
This skill guides the conversion of ASP.NET 4.8 WebMethods (from LargoV3/) to .NET Core 8 API endpoints (in AiLA_WebAPI/).
Step 1: Analyze the Source WebMethod
Before converting, identify:
- Source file path (e.g.,
LargoV3/SomePage.aspx.cs) - Method name and parameters
- Data access pattern (raw SQL, Entity Framework, stored procedure)
- Return type (anonymous object, string, specific type)
- Authorization requirements
Step 2: Create Required Files
For each conversion, create these files in AiLA_WebAPI/AiLA_WebAPI/:
2.1 Request DTO (if method has parameters)
File: DTOs/{FeatureName}Dtos.cs (or add to existing)
namespace AiLA_WebAPI.DTOs
{
public class {MethodName}RequestDto
{
public string Param1 { get; set; }
public string Param2 { get; set; }
// Map all WebMethod parameters as properties
}
}
2.2 Response DTO (for complex return types)
namespace AiLA_WebAPI.DTOs
{
public class {MethodName}ResponseDto
{
// Map all properties from the anonymous object or return type
public string Property1 { get; set; }
public List<SubItemDto> Items { get; set; }
}
}
2.3 Repository Interface
File: Repositories/Interfaces/I{FeatureName}Repository.cs
namespace AiLA_WebAPI.Repositories.Interfaces
{
public interface I{FeatureName}Repository
{
Task<{ResponseDto}> {MethodName}Async({RequestDto} request);
// Or for simple returns:
Task<string> {MethodName}Async(string param1, string param2);
}
}
2.4 Repository Implementation
File: Repositories/{FeatureName}Repository.cs
using AiLA_WebAPI.DTOs;
using AiLA_WebAPI.Repositories.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace AiLA_WebAPI.Repositories
{
public class {FeatureName}Repository : I{FeatureName}Repository
{
private readonly largomldbEntities _context;
private readonly IConfiguration _configuration;
public {FeatureName}Repository(largomldbEntities context, IConfiguration configuration)
{
_context = context;
_configuration = configuration;
}
public async Task<{ResponseDto}> {MethodName}Async({RequestDto} request)
{
// Convert the data access logic here
// Use async/await patterns
// Return strongly-typed DTOs
}
}
}
2.5 Controller
File: Controllers/{FeatureName}Controller.cs
using AiLA_WebAPI.DTOs;
using AiLA_WebAPI.Models;
using AiLA_WebAPI.Repositories.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AiLA_WebAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize] // Add if authentication required
public class {FeatureName}Controller : BaseApiController
{
private readonly I{FeatureName}Repository _repository;
private readonly ILogger<{FeatureName}Controller> _logger;
public {FeatureName}Controller(
I{FeatureName}Repository repository,
ILogger<{FeatureName}Controller> logger)
{
_repository = repository;
_logger = logger;
}
/// <summary>
/// {Description of what the endpoint does}
/// </summary>
[HttpPost("{MethodName}")]
[ProducesResponseType(typeof(ApiResponse<{ResponseDto}>), StatusCodes.Status200OK)]
public async Task<IActionResult> {MethodName}([FromBody] {MethodName}RequestDto request)
{
// Validate required parameters (throw ValidationException for invalid input)
if (string.IsNullOrWhiteSpace(request.RequiredParam))
{
throw new ValidationException("RequiredParam is required", nameof(request.RequiredParam));
}
var result = await _repository.{MethodName}Async(request);
return Ok(ApiResponse<{ResponseDto}>.CreateSuccess(result, "Operation completed successfully"));
}
}
}
Step 3: Register Repository in DI
File: AiLA_WebAPI/ServiceCollectionExtensions.cs
Add the repository registration:
services.AddScoped<I{FeatureName}Repository, {FeatureName}Repository>();
Conversion Patterns Reference
Pattern 1: Anonymous Object Return → ApiResponse
OLD (ASP.NET 4.8):
[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public object GetData(string param1)
{
try
{
// ... data access ...
return new { success = true, data = result };
}
catch (Exception ex)
{
return new { success = false, message = ex.Message };
}
}
NEW (.NET Core):
[HttpPost("GetData")]
[ProducesResponseType(typeof(ApiResponse<DataDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetData([FromBody] GetDataRequestDto request)
{
var result = await _repository.GetDataAsync(request);
return Ok(ApiResponse<DataDto>.CreateSuccess(result, "Data retrieved successfully"));
}
Note: No try-catch needed -
GlobalExceptionHandlingMiddlewarehandles all exceptions automatically.
Pattern 2: Manual SQL → Repository with Stored Procedure
OLD (ASP.NET 4.8):
[WebMethod]
public static string GetDetails(string id)
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand("SELECT * FROM Table WHERE Id = @Id", conn))
{
cmd.Parameters.AddWithValue("@Id", id);
// ... execute and return
}
}
}
NEW (.NET Core Repository):
public async Task<DetailsDto> GetDetailsAsync(string id)
{
using (var connection = _context.Database.GetDbConnection())
{
await connection.OpenAsync();
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT * FROM Table WHERE Id = @Id";
command.Parameters.Add(new SqlParameter("@Id", id));
using (var reader = await command.ExecuteReaderAsync())
{
if (await reader.ReadAsync())
{
return new DetailsDto
{
Property1 = reader["Column1"]?.ToString(),
// ... map other properties
};
}
}
}
}
return null;
}
Pattern 3: Entity Framework Inline → Repository with Async EF
OLD (ASP.NET 4.8):
[WebMethod]
public static string GetItems(string filter)
{
using (largomldbEntities context = new largomldbEntities())
{
var items = context.SomeTable
.Where(x => x.Name.Contains(filter))
.ToList();
return JsonConvert.SerializeObject(new { success = true, data = items });
}
}
NEW (.NET Core Repository):
public async Task<List<ItemDto>> GetItemsAsync(string filter)
{
return await _context.SomeTable
.AsNoTracking()
.Where(x => x.Name.Contains(filter))
.Select(x => new ItemDto
{
Id = x.Id,
Name = x.Name
})
.ToListAsync();
}
Pattern 4: HttpContext.Current.User → Claims-based Identity
OLD (ASP.NET 4.8):
string username = HttpContext.Current.User.Identity.Name;
if (!AuthorizationHelper.IsAuthorizedUser()) return null;
NEW (.NET Core Controller):
// In controller (inherits BaseApiController)
var username = GetCurrentUser(); // Helper method from BaseApiController
// Or access claims directly:
var email = User.FindFirst(ClaimTypes.Email)?.Value;
Pattern 5: LoadDropdowns/Page_Load → InitialLoad Endpoint
IMPORTANT: Many ASPX pages have LoadDropdowns() methods called in Page_Load() that populate dropdowns server-side. These MUST be converted to API endpoints.
OLD (ASP.NET 4.8 - Page_Load with LoadDropdowns):
protected void Page_Load(object sender, EventArgs e)
{
LoadDropdowns();
}
private void LoadDropdowns()
{
using (largomldbEntities context = new largomldbEntities())
{
var portfolioNames = context.portfolio_config
.AsNoTracking()
.Where(c => !c.portfolio_name.ToLower().Contains("template"))
.Select(c => c.portfolio_name)
.Distinct()
.ToList();
// Add metadata to dropdown items via data-* attributes
var pmcodePortfolios = context.pmcode_portfolio_config
.AsNoTracking()
.Where(p => portfolioNames.Contains(p.portfolio_name))
.ToDictionary(p => p.portfolio_name, p => p);
ddlPortfolio.Items.Add(new ListItem("--Select--", "-1"));
ddlPortfolio.Items.AddRange(
portfolioNames.Select(name => {
ListItem item = new ListItem(name, name);
if (pmcodePortfolios.TryGetValue(name, out var config))
{
item.Attributes.Add("data-pm-id", config.id.ToString());
item.Attributes.Add("data-pm-is-active", config.is_active.ToString().ToLower());
item.Attributes.Add("data-pm-aum", config.portfolio_aum?.ToString() ?? "");
}
return item;
}).ToArray()
);
}
}
NEW (.NET Core - InitialLoad/GetDropdowns endpoint):
DTO (DTOs/{Feature}Dtos.cs):
public class PortfolioDropdownDto
{
public string Name { get; set; }
public long? ConfigId { get; set; }
public bool? IsActive { get; set; }
public decimal? Aum { get; set; }
}
public class {Feature}InitialLoadResponseDto
{
public List<PortfolioDropdownDto> Portfolios { get; set; }
// Add other dropdown lists as needed
}
Repository:
public async Task<{Feature}InitialLoadResponseDto> GetInitialLoadAsync()
{
var portfolioNames = await _context.portfolio_config
.AsNoTracking()
.Where(c => !c.portfolio_name.ToLower().Contains("template"))
.Select(c => c.portfolio_name)
.Distinct()
.ToListAsync();
var pmcodeConfigs = await _context.pmcode_portfolio_config
.AsNoTracking()
.Where(p => portfolioNames.Contains(p.portfolio_name))
.ToDictionaryAsync(p => p.portfolio_name, p => p);
return new {Feature}InitialLoadResponseDto
{
Portfolios = portfolioNames.Select(name => new PortfolioDropdownDto
{
Name = name,
ConfigId = pmcodeConfigs.TryGetValue(name, out var config) ? config.id : null,
IsActive = pmcodeConfigs.TryGetValue(name, out config) ? config.is_active : null,
Aum = pmcodeConfigs.TryGetValue(name, out config) ? config.portfolio_aum : null
}).ToList()
};
}
Controller:
/// <summary>
/// Get initial load data for dropdowns and form defaults
/// </summary>
[HttpGet("InitialLoad")]
[ProducesResponseType(typeof(ApiResponse<{Feature}InitialLoadResponseDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetInitialLoad()
{
var result = await _repository.GetInitialLoadAsync();
return Ok(ApiResponse<{Feature}InitialLoadResponseDto>.CreateSuccess(result, "Initial data loaded successfully"));
}
Key Points:
- Use
[HttpGet]since this is read-only data retrieval- Name the endpoint
InitialLoad,GetDropdowns, orGetPageData- Include ALL dropdown data needed for the page in ONE response
- Convert
data-*attributes to DTO properties- Frontend calls this endpoint on page load to populate dropdowns
Pattern 6: Stored Procedure with Multiple Result Sets
OLD (ASP.NET 4.8):
[WebMethod]
public static string GetReport(string date)
{
using (SqlConnection conn = new SqlConnection(connStr))
{
SqlDataAdapter adapter = new SqlDataAdapter("usp_GetReport", conn);
adapter.SelectCommand.CommandType = CommandType.StoredProcedure;
adapter.SelectCommand.Parameters.AddWithValue("@Date", date);
DataSet ds = new DataSet();
adapter.Fill(ds);
return JsonConvert.SerializeObject(new {
table1 = ds.Tables[0],
table2 = ds.Tables[1]
});
}
}
NEW (.NET Core Repository):
public async Task<ReportDto> GetReportAsync(string date)
{
var result = new ReportDto();
using (var connection = _context.Database.GetDbConnection())
{
await connection.OpenAsync();
using (var command = connection.CreateCommand())
{
command.CommandText = "usp_GetReport";
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add(new SqlParameter("@Date", date));
using (var adapter = new SqlDataAdapter((SqlCommand)command))
{
var dataSet = new DataSet();
await Task.Run(() => adapter.Fill(dataSet));
// Map first result set
if (dataSet.Tables.Count > 0)
{
result.Table1Data = dataSet.Tables[0].AsEnumerable()
.Select(row => new Table1Dto
{
Column1 = row["Column1"]?.ToString(),
Column2 = row["Column2"]?.ToString()
}).ToList();
}
// Map second result set
if (dataSet.Tables.Count > 1)
{
result.Table2Data = dataSet.Tables[1].AsEnumerable()
.Select(row => new Table2Dto
{
// ... map columns
}).ToList();
}
}
}
}
return result;
}
HTTP Method Selection Guide
| WebMethod Behavior | .NET Core Attribute |
|---|---|
| Retrieves data without side effects | [HttpGet] |
| Creates new resources | [HttpPost] |
| Updates existing resources | [HttpPut] or [HttpPatch] |
| Deletes resources | [HttpDelete] |
| Complex queries with body | [HttpPost] |
| Simple parameter queries | [HttpGet] with [FromQuery] |
Note: Most legacy WebMethods should use [HttpPost] since they typically accept complex parameters and may have side effects.
Query Parameter vs Body Parameter
Use [FromQuery] for simple GET requests:
[HttpGet("GetItem")]
[ProducesResponseType(typeof(ApiResponse<ItemDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetItem(
[FromQuery] string id,
[FromQuery] string type = "default")
Use [FromBody] for complex data or POST requests:
[HttpPost("SaveItem")]
[ProducesResponseType(typeof(ApiResponse<SaveResultDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> SaveItem([FromBody] SaveItemRequestDto request)
Checklist for Each Conversion
- Identify source WebMethod file and method
- Check for
LoadDropdowns()inPage_Load()- if present, createGET InitialLoadendpoint - Create Request DTO (if parameters exist)
- Create Response DTO (for return type)
- Create or update Repository Interface
- Implement Repository method with async pattern
- Create or update Controller
- Register repository in DI container
- Add appropriate authorization (
[Authorize]or remove if public) - Add XML documentation comments
- Add
[ProducesResponseType]for 200 OK (for Swagger) - Test endpoint with Swagger UI
- Update frontend JavaScript to call new API endpoint
Existing Infrastructure Reference
Global Exception Handling
The API uses GlobalExceptionHandlingMiddleware which catches all unhandled exceptions. Do NOT wrap controller methods in try-catch blocks. Instead:
- Throw
ValidationExceptionfor input validation errors (400) - Throw
NotFoundExceptionfor missing resources (404) - Throw
BadRequestExceptionfor bad requests (400) - Let other exceptions bubble up (automatically becomes 500)
BaseApiController Helper Methods
GetCurrentUser()- Returns authenticated user's email/nameApiSuccess<T>(data, message)- Create success responseApiError<T>(message, statusCode, errors)- Create error response
ApiResponse Factory Methods
ApiResponse<T>.CreateSuccess(data, message)- Success wrapperApiResponse<T>.CreateError(message, statusCode, errorCode, errorMessage, field)- Error wrapper
DbContext Available
largomldbEntities- Main database context (registered in DI)AilaIndicesdbEntities- Indices database context
Database Schema Prefixes
- Tracker tables: Always use
Tracker.schema prefix (e.g.,Tracker.tracker_portfolio_config,Tracker.tracker_plus_benchmark_config) - Default schema (dbo): No prefix needed for most tables
Example: Complete Conversion
Source WebMethod (LargoV3/SomePage.aspx.cs)
[WebMethod]
public static string GetPortfolioAllocation(string portfolioName)
{
try
{
if (!AuthorizationHelper.IsAuthorizedUser()) return null;
using (largomldbEntities context = new largomldbEntities())
{
var allocations = context.portfolio_allocations
.Where(a => a.portfolio_name == portfolioName)
.Select(a => new { a.asset_name, a.weight })
.ToList();
return JsonConvert.SerializeObject(new { success = true, data = allocations });
}
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
Converted to .NET Core
DTO (DTOs/PortfolioAllocationDtos.cs):
namespace AiLA_WebAPI.DTOs
{
public class GetPortfolioAllocationRequestDto
{
public string PortfolioName { get; set; }
}
public class PortfolioAllocationDto
{
public string AssetName { get; set; }
public decimal Weight { get; set; }
}
}
Repository Interface (Repositories/Interfaces/IPortfolioRepository.cs):
public interface IPortfolioRepository
{
Task<List<PortfolioAllocationDto>> GetPortfolioAllocationAsync(string portfolioName);
}
Repository (Repositories/PortfolioRepository.cs):
public class PortfolioRepository : IPortfolioRepository
{
private readonly largomldbEntities _context;
public PortfolioRepository(largomldbEntities context)
{
_context = context;
}
public async Task<List<PortfolioAllocationDto>> GetPortfolioAllocationAsync(string portfolioName)
{
return await _context.portfolio_allocations
.AsNoTracking()
.Where(a => a.portfolio_name == portfolioName)
.Select(a => new PortfolioAllocationDto
{
AssetName = a.asset_name,
Weight = a.weight
})
.ToListAsync();
}
}
Controller (Controllers/PortfolioController.cs):
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class PortfolioController : BaseApiController
{
private readonly IPortfolioRepository _repository;
private readonly ILogger<PortfolioController> _logger;
public PortfolioController(IPortfolioRepository repository, ILogger<PortfolioController> logger)
{
_repository = repository;
_logger = logger;
}
/// <summary>
/// Get portfolio allocation by portfolio name
/// </summary>
[HttpPost("GetPortfolioAllocation")]
[ProducesResponseType(typeof(ApiResponse<List<PortfolioAllocationDto>>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetPortfolioAllocation([FromBody] GetPortfolioAllocationRequestDto request)
{
if (string.IsNullOrWhiteSpace(request.PortfolioName))
{
throw new ValidationException("PortfolioName is required", nameof(request.PortfolioName));
}
var result = await _repository.GetPortfolioAllocationAsync(request.PortfolioName);
return Ok(ApiResponse<List<PortfolioAllocationDto>>.CreateSuccess(
result, "Portfolio allocation retrieved successfully"));
}
}
Register in DI (ServiceCollectionExtensions.cs):
services.AddScoped<IPortfolioRepository, PortfolioRepository>();
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です