01Enterprise .NET× and 02Agentic AI× and 03CloudBuilt the proven way · shipped faster with AI agents · deployed on Azure, AWS and Google Cloud
Senior .NET, agentic AI & front-end engineer for government and healthcare systems.
12+ years building C# services and the interfaces people use to reach them, for the U.S. Department of Justice, Department of Energy, the FAA and K-12 education. I modernize legacy ASP.NET, MVC and Web Forms into .NET 8 and Angular, built to WCAG 2.1 AA and Section 508. Today I also build AI agents and the secure .NET services they call, deployed on Azure, AWS and Google Cloud.
Back end, front end, agentic AI and cloud, in one developer
.NET & C#
ASP.NET Core Web APIs, MVC and Web Forms modernization, EF Core, SignalR and Blazor.
.NET 8ASP.NET CoreMVCEF Core
Front end & UI
Angular and React interfaces built from Figma and Adobe XD designs on Material, Bootstrap and PrimeNG.
Angular 18TypeScriptHTML5 / CSS3Figma
Accessibility
Section 508 and WCAG 2.1 AA delivered on DOJ and DOE systems, using WAI-ARIA patterns.
WCAG 2.1 AASection 508WAI-ARIA
Data & delivery
SQL Server and T-SQL, REST and GraphQL APIs, and CI/CD pipelines in Azure DevOps.
SQL ServerAzure DevOpsDocker / AKSAWS
Agentic AI
MCP tool servers in C#, tool-calling assistants, RAG and scheduled agents, all behind tests, CI gates and human review.
MCPClaudeAzure OpenAIRAG
Cloud
Azure App Service, Functions, Key Vault and AKS; AWS Lambda, EC2 and S3; Google Cloud Functions and Firebase.
AzureAWSGoogle CloudKubernetes
Selected work
Public-sector and regulated systems, modernized
Six engagements across justice, energy, aviation, education, insurance and healthcare.
Federal · Justice2016–2020
U.S. Department of Justice: accessible modernization
10+ legacy systems modernized
Rebuilt aging ASP.NET applications as responsive Angular and .NET Core apps that meet the department's accessibility mandates.
Section 508 and WCAG 2.1 AA built into each migrated app
25+ secure REST APIs for HIPAA workflows with OAuth2 and PII encryption
Rotated as production support engineer on live incidents
.NET CoreAngularWAI-ARIASQL Server
Public education2025–present
K-12 enrollment management platform
99.9% uptime via Azure DevOps to AKS
An enrollment system for K-12 programs that predicts student placement, built on .NET 8, Angular 18 and Azure OpenAI.
REST and GraphQL APIs behind Azure API Management
Containerized services with CI/CD pipelines in Azure DevOps
Datadog monitoring for API health and production incidents
.NET 8Angular 18AKSAzure OpenAI
Federal · Aviation2024–2025
FAA budgeting and reporting tools
60% faster dashboard load times
Tuned .NET 8 Web APIs and Angular 18 rendering for enterprise reporting, and added an LLM-assisted budgeting workflow.
70% less manual processing in budget analysis
Blazor apps for real-time internal data views
Event-driven services across 10+ microservices
.NET 8Angular 18BlazorAWS Lambda
Federal · Energy2020–2021
Department of Energy: grant management
AA WCAG 2.1 conformance target
Maintained ASP.NET MVC and .NET Core grant and budgeting systems and rebuilt their Angular front ends from Figma designs.
Accessible PrimeNG and AG-Grid data views with WAI-ARIA
Azure AD SSO with JWT
Tests with MSTest, Moq, Jasmine and Karma; mentored junior engineers
ASP.NET MVCAngularPrimeNGFigma
Insurance2023–2024
Intact Insurance: micro front ends
>95% test coverage with xUnit
Angular 17 front end on .NET microservice APIs, built as micro front ends with Module Federation.
Standalone Angular components
Test-driven .NET Core services
Azure DevOps YAML pipelines and PowerShell automation
Angular 17xUnitPostgreSQLAzure DevOps
Healthcare · FDA2015–2016
FDA-integrated drug management portal
HIPAA compliant drug labeling portal
A HIPAA-compliant portal that streamlines drug labeling, compliance review and approvals, with structured regulatory content rendered to HTML.
ASP.NET MVC and Web API on Oracle
XML with XSD validation, rendered to accessible HTML
Real-time drug data exchange for regulatory reporting
ASP.NET MVCXML / XSLTOracle
Agentic AI applications
Four agentic AI applications I designed, built and run in production
My own products, outside client work. In each one, AI agents do real work on a schedule or on request: they call tools, write, check and publish, with automated tests and a CI/CD pipeline behind them. The repositories are private; I can walk through the code in an interview.
Agentic AI · Data analytics platformWeb · iOS · Android
Crosstab
A public-opinion analytics platform. Every morning it collects new survey data, rebuilds rolling averages and a probability forecast, publishes the site and apps, and sends personalized alerts to subscribers.
Agentic AI · Compliance · healthcare trainingiOS · Android · Web
Fieldwise
A fieldwork-hours tracking and compliance system for behavior-analyst trainees, supervisors and organizations. It checks each month against BACB requirements and fills the official verification forms.
Offline-first app with Supabase sync and row-level security for each trainee
In-house e-signature engine: signing envelopes, audit trail and a PAdES certification seal
Shared rules engine fills the official BACB PDF forms field by field, with no redrawing
AI agents
Claude voice assistant with tool use, directory research agent on Fly.io
Front end
TypeScript, React Native, Expo Router, Astro
Back end
Supabase Edge Functions (Deno), Fly.io workers
Data
PostgreSQL with RLS, Supabase Storage
Delivery
GitHub Actions, Vitest, Stripe, pdf-lib, Claude API
21 test filesfieldwiseaba.com
ArchitectureFieldwise
Clients
AppExpo · iOS, Android, web
Marketing siteAstro · SEO
Shared packages
Domain
Rules engineBACB checks · PDF forms
E-signaturePAdES seal · RFC 3161
Voice assistantOffline parser + Claude
Sync · HTTPS
Back end
Edge Functionsesign · checkout · assistant
WorkersFly.io · directory, email
SQL · RLS
Data
PostgresRow-level security
StorageForm templates · PDFs
BillingStripe webhooks
Agentic AI · Real-time monitoringPWA · Android
VisaPulse
Monitors US visa appointment availability and alerts users the minute a date opens. An installable app on a Python API, with a single background checker that is careful with the source site.
Checker reuses sessions, spaces checks by plan, backs off on errors and removes duplicate alerts
Saved credentials are encrypted in a vault with rotatable keys
CI tests on SQLite and PostgreSQL 16, then deploys with a backup, health check and automatic rollback
AI agents
Monitoring agent with backoff and dedupe, GPT assistant with web search and live voice
An autonomous social-media engine sold as multi-brand SaaS. It learns a brand from its app, website and files, plans a calendar for each market's time zone, then writes, designs and publishes posts.
Writer may only cite facts that appear word for word in the brand's own sources
One Docker image runs two Fly.io process groups (API and workers) with rolling deploys and migrations run before release
Monorepo with a typed API client shared by the web and native apps
AI agents
Plan, write, design, publish and measure agents, limited to verified facts
Front end
TypeScript, React, Vite, Expo (iOS, Android)
Back end
Node.js, Fastify, Zod, Helmet, rate limiting
Data
PostgreSQL (Supabase)
Delivery
Fly.io, GitHub Actions, Vitest, Satori, Claude API
13 test filesIn development
ArchitectureHookline
Clients
DashboardReact · Vite
MobileExpo
Connected appsPOST /v1/signals
Typed REST client
API
Fastify APIMulti-brand · Zod
Brand profileVerified facts only
Job tables
Engine
WorkersPlan · write · design · publish · measure
Image renderSatori · resvg
SQL · APIs
Data · services
PostgresSupabase
LLMClaude API
NetworksMeta · X · LinkedIn · TikTok
Agentic development
I build AI agents, and I build with them
Agents that call real tools, held to the same standard as any production code: typed inputs, tests, CI gates and a person in the loop where it matters.
Agents and AI features I have shipped
Client · .NET 8MCP tool servers in C#Exposed K-12 enrollment, eligibility, scholarship and admin workflows as secure tools that an AI model can call, using the .NET MCP SDK.
Client · Azure OpenAIEnterprise LLM featuresStudent placement prediction for a K-12 enrollment system, and a budgeting assistant at the FAA that cut manual processing by 70%.
Fieldwise · tool useVoice logging assistantClaude turns what a trainee says into draft sessions through one tool. An on-device parser takes over offline, and nothing saves until the user confirms.
Crosstab · scheduled agentsSelf-running data pipelineAgents collect, check and publish new data every morning, write the summary and send the alerts with no manual steps.
Hookline · guardrailsA brand engine that can't make things upPlans, writes, designs and posts, but may only cite facts that appear word for word in the brand's own sources.
How I work with AI coding agents
Spec firstWrite the task, the limits and the acceptance tests before any code.
Agent draftsClaude Code, GitHub Copilot or Codex makes the change on a branch.
Gates decideCI runs tests, type checks, migrations and accessibility scans. A failure blocks the merge.
I review every diffEspecially security, data handling and accessibility, before anything ships.
Seven files, from the database to an AI agent's tool server to the deployment pipeline. Pick a tab to read one.
Controllers/ApplicantsController.csValidation returns RFC 7807 problem details. PII is encrypted with ASP.NET Core Data Protection before it reaches SQL Server and never written to logs.
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Agency.Services.Applicants;
public sealed record CreateApplicantRequest(
[property: Required, StringLength(100)] string FullName,
[property: Required, EmailAddress] string Email,
[property: Required, RegularExpression(@"^\d{3}-\d{2}-\d{4}$",
ErrorMessage = "Enter the SSN as 123-45-6789.")] string Ssn);
public sealed record ApplicantDto(int Id, string FullName, string Email, string SsnMasked)
{
public static ApplicantDto From(Applicant a) =>
new(a.Id, a.FullName, a.Email, $"***-**-{a.SsnLast4}");
}
[ApiController]
[Route("api/applicants")]
[Authorize(Policy = "CaseWorker")]
public sealed class ApplicantsController(
AppDbContext db,
IDataProtectionProvider protection,
ILogger<ApplicantsController> log) : ControllerBase
{
// Purpose string isolates this key ring; rotate by bumping the version.
private readonly IDataProtector _pii = protection.CreateProtector("Applicants.Pii.v1");
[HttpPost]
[ProducesResponseType<ApplicantDto>(StatusCodes.Status201Created)]
[ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(CreateApplicantRequest req, CancellationToken ct)
{
// [ApiController] has already returned 400 + ValidationProblemDetails
// if any annotation above failed, so the request is valid here.
if (await db.Applicants.AnyAsync(a => a.Email == req.Email.ToLowerInvariant(), ct))
{
ModelState.AddModelError(nameof(req.Email), "An applicant with this email already exists.");
return ValidationProblem(ModelState);
}
var entity = new Applicant
{
FullName = req.FullName.Trim(),
Email = req.Email.Trim().ToLowerInvariant(),
SsnCipher = _pii.Protect(req.Ssn),
SsnLast4 = req.Ssn[^4..]
};
db.Applicants.Add(entity);
await db.SaveChangesAsync(ct);
log.LogInformation("Applicant {ApplicantId} created", entity.Id); // ID only, no PII
return CreatedAtAction(nameof(GetById), new { id = entity.Id }, ApplicantDto.From(entity));
}
[HttpGet("{id:int}")]
[ProducesResponseType<ApplicantDto>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApplicantDto>> GetById(int id, CancellationToken ct)
{
var applicant = await db.Applicants.AsNoTracking()
.SingleOrDefaultAsync(a => a.Id == id, ct);
return applicant is null ? NotFound() : ApplicantDto.From(applicant);
}
}
Enrollment.Mcp/Program.csAn ASP.NET Core MCP server that lets an AI agent query enrollment data through narrow, read-only, authorized tools. The agent never gets a database connection.
using System.ComponentModel;
using Microsoft.EntityFrameworkCore;
using ModelContextProtocol;
using ModelContextProtocol.Server;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddJwtBearer(); // Entra ID access tokens
builder.Services.AddAuthorization(o =>
o.AddPolicy("EnrollmentReader", p => p.RequireRole("Enrollment.Read")));
builder.Services.AddDbContext<EnrollmentDb>(o =>
o.UseSqlServer(builder.Configuration.GetConnectionString("Enrollment")));
builder.Services
.AddMcpServer()
.WithHttpTransport()
.WithTools<EnrollmentTools>();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapMcp("/mcp").RequireAuthorization("EnrollmentReader");
app.Run();
/// <summary>Tools an AI agent may call. Each is narrow, validated and read-only.</summary>
[McpServerToolType]
public sealed class EnrollmentTools(EnrollmentDb db, ILogger<EnrollmentTools> log)
{
[McpServerTool(ReadOnly = true)]
[Description("Returns open seats for one school and grade in a given school year.")]
public async Task<SeatAvailability> GetSeatAvailability(
[Description("State school ID, for example 0123-4567")] string schoolId,
[Description("Grade level: 0 for kindergarten, up to 12")] int grade,
[Description("School year it starts in, for example 2027")] int schoolYear,
CancellationToken ct)
{
if (grade is < 0 or > 12)
throw new McpException($"Grade {grade} is out of range. Use 0 (kindergarten) to 12.");
var seats = await db.Seats.AsNoTracking()
.Where(s => s.SchoolId == schoolId && s.Grade == grade && s.SchoolYear == schoolYear)
.Select(s => new SeatAvailability(s.SchoolId, s.Grade, s.Capacity, s.Enrolled, s.Capacity - s.Enrolled))
.SingleOrDefaultAsync(ct)
?? throw new McpException($"No seat plan for school {schoolId}, grade {grade}, {schoolYear}.");
log.LogInformation("Seat lookup {SchoolId} grade {Grade}", schoolId, grade); // no student data in logs
return seats;
}
}
public sealed record SeatAvailability(string SchoolId, int Grade, int Capacity, int Enrolled, int Open);
TagHelpers/AccessibleFieldTagHelper.csOne tag, <a11y-field asp-for="Email" hint="…" />, renders the label, hint, error and input, all linked. MVC and Razor Pages teams get correct markup by default.
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace Agency.Web.TagHelpers;
/// <summary>
/// Renders a design-system text field: label, optional hint, inline error, input.
/// Wires for/id, aria-describedby and aria-invalid so WCAG 1.3.1, 3.3.1 and
/// 4.1.2 are met without each developer remembering to do it.
/// </summary>
[HtmlTargetElement("a11y-field", Attributes = "asp-for", TagStructure = TagStructure.WithoutEndTag)]
public sealed class AccessibleFieldTagHelper(IHtmlGenerator generator, HtmlEncoder encoder) : TagHelper
{
[HtmlAttributeName("asp-for")] public ModelExpression For { get; set; } = default!;
[HtmlAttributeName("hint")] public string? Hint { get; set; }
[HtmlAttributeName("type")] public string InputType { get; set; } = "text";
[HtmlAttributeName("autocomplete")] public string? AutoComplete { get; set; }
[ViewContext, HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; } = default!;
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var name = For.Name;
var id = TagBuilder.CreateSanitizedId(name, "_");
var hintId = $"{id}-hint";
var errorId = $"{id}-error";
var errors = ViewContext.ViewData.ModelState[name]?.Errors;
var hasError = errors is { Count: > 0 };
var describedBy = string.Join(' ',
new[] { Hint is null ? null : hintId, hasError ? errorId : null }.OfType<string>());
var input = generator.GenerateTextBox(ViewContext, For.ModelExplorer, name,
For.Model, format: null, htmlAttributes: new { @class = "ds-input" });
input.Attributes["id"] = id;
input.Attributes["type"] = InputType;
if (describedBy.Length > 0) input.Attributes["aria-describedby"] = describedBy;
if (hasError) input.Attributes["aria-invalid"] = "true";
if (For.Metadata.IsRequired) input.Attributes["required"] = "required";
if (AutoComplete is not null) input.Attributes["autocomplete"] = AutoComplete;
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
output.Attributes.SetAttribute("class", hasError ? "ds-field ds-field--error" : "ds-field");
var label = encoder.Encode(For.Metadata.GetDisplayName());
output.Content.AppendHtml($"<label class=\"ds-label\" for=\"{id}\">{label}</label>");
if (Hint is not null)
output.Content.AppendHtml($"<p class=\"ds-hint\" id=\"{hintId}\">{encoder.Encode(Hint)}</p>");
if (hasError)
output.Content.AppendHtml(
$"<p class=\"ds-error\" id=\"{errorId}\">" +
$"<span class=\"visually-hidden\">Error: </span>{encoder.Encode(errors![0].ErrorMessage)}</p>");
output.Content.AppendHtml(input);
}
}
styles/design-system/_tokens.scssComponents use tokens, never raw hex values. The text-on mixin computes the WCAG contrast ratio while Sass compiles and fails the build if a pairing falls below AA.
ui/sortable-table.jsAn accessible sortable table with no framework. It improves a plain HTML table, so the content still works if the script fails.
/**
* Accessible sortable table.
* - Header buttons are real <button>s: focusable, Enter/Space for free.
* - aria-sort lives on the <th> (only the active column carries it).
* - A polite live region announces the result; focus stays on the button.
*/
export function sortableTable(table, { rows, render, status }) {
const headers = [...table.querySelectorAll('thead th[data-key]')];
let state = { key: null, dir: 'ascending' };
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
const compare = (a, b, key) =>
typeof a[key] === 'number' ? a[key] - b[key] : collator.compare(a[key], b[key]);
function sortBy(key) {
state = {
key,
dir: state.key === key && state.dir === 'ascending' ? 'descending' : 'ascending',
};
const sign = state.dir === 'ascending' ? 1 : -1;
const sorted = [...rows()].sort((a, b) => sign * compare(a, b, key));
headers.forEach(th => {
if (th.dataset.key === key) th.setAttribute('aria-sort', state.dir);
else th.removeAttribute('aria-sort');
});
render(sorted);
const label = headers.find(th => th.dataset.key === key).textContent.trim();
status.textContent = `Sorted by ${label}, ${state.dir}. ${sorted.length} rows.`;
}
headers.forEach(th =>
th.querySelector('button').addEventListener('click', () => sortBy(th.dataset.key)));
return { sortBy, get state() { return { ...state }; } };
}
// Status cells pair text with color so meaning survives without color (WCAG 1.4.1).
export const statusCell = s =>
`<span class="pill pill--${s.toLowerCase()}">${s}</span>`;
db/procs/usp_ServiceRequest_Search.sqlA fully parameterized search with optional filters and keyset paging. It cannot be injected and keeps a stable plan on large case tables.
CREATE OR ALTER PROCEDURE dbo.usp_ServiceRequest_Search
@Status varchar(20) = NULL, -- NULL = any status
@CountyId int = NULL,
@SubmittedFrom date = NULL,
@AfterId int = 0, -- keyset cursor: last Id from previous page
@PageSize int = 25
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF @PageSize NOT BETWEEN 1 AND 100
THROW 50001, N'PageSize must be between 1 and 100.', 1;
SELECT TOP (@PageSize)
sr.Id,
sr.CaseNumber,
p.ProgramName,
c.CountyName,
sr.Status,
sr.SubmittedOn,
DATEDIFF(day, sr.SubmittedOn, SYSUTCDATETIME()) AS DaysOpen
FROM dbo.ServiceRequest AS sr
JOIN dbo.Program AS p ON p.Id = sr.ProgramId
JOIN dbo.County AS c ON c.Id = sr.CountyId
WHERE sr.Id > @AfterId
AND (@Status IS NULL OR sr.Status = @Status)
AND (@CountyId IS NULL OR sr.CountyId = @CountyId)
AND (@SubmittedFrom IS NULL OR sr.SubmittedOn >= @SubmittedFrom)
ORDER BY sr.Id
OPTION (RECOMPILE); -- optional filters: plan built for the actual values
END;
GO
-- Supporting index: seek on Id, cover the filtered columns.
CREATE INDEX IX_ServiceRequest_Status_County
ON dbo.ServiceRequest (Status, CountyId)
INCLUDE (ProgramId, SubmittedOn, CaseNumber);
azure-pipelines.ymlBuild, test, compile Sass and run an axe accessibility scan against the built site. A WCAG 2.1 AA violation fails the pipeline the same way a failing unit test does.