基于C#的在线编码与自动化测试全栈Web平台的设计与实现
摘要
随着软件工程教育和技术面试的数字化转型,在线编程与自动化评测平台的需求日益增长。本文设计并实现了一个基于C#开发语言的全栈Web平台,支持用户在线编写C#代码、提交代码并在服务器端自动编译执行和运行单元测试,实时反馈测试结果。平台前端采用React构建交互式代码编辑器,后端基于ASP.NET Core Web API,利用Roslyn编译器实现动态编译与沙箱执行,并通过xUnit框架自动化运行用户提交代码的测试用例。系统整体采用前后端分离架构,引入JWT身份认证、SQLite持久化存储以及Docker容器隔离执行环境,保障安全性、可扩展性和易部署性。本文详细阐述了平台的需求分析、架构设计、技术选型、核心模块实现思路,并给出了一个可运行的最小化原型系统的完整代码示例,验证了设计方案的可行性。
**关键词**:在线编程;自动化测试;C#;ASP.NET Core;Roslyn;全栈Web平台
第1章 绪论
1.1 研究背景与意义
在编程教学、技术笔试和开发者技能评估等场景中,在线编码平台已成为核心工具。当前主流的在线评判系统(如LeetCode、HackerRank)多支持Java、Python、C++等语言,对C#的支持往往停留在简单代码执行,缺乏深度集成的自动化单元测试反馈。同时,在企业内部培训和.NET技术栈的教学中,亟需一个支持C#在线编码、并能自定义测试用例、即时验证代码正确性的平台。
开发这样一个基于C#的全栈Web平台,不仅能让学生或面试者在浏览器中直接编写和运行C#代码,还能通过预定义的xUnit测试用例自动评估代码质量,显著提升学习效率和评估客观性。从技术角度看,该平台整合了前端交互、后端服务、动态编译、沙箱隔离和自动化测试等多项技术,是一个典型的全栈工程实践项目,具有较高的研究与应用价值。
1.2 国内外研究现状
现有在线编程平台可大致分为两类:一类是以LeetCode、NowCoder为代表的通用刷题平台,提供了丰富的题库和在线评测,但后端评测环境多为黑盒,不支持用户自定义测试框架;另一类是以GitHub Codespaces、Replit为代表的云端开发环境,虽支持完整的开发流程,但资源消耗大、延迟较高,不适用于快速测评场景。针对C#的在线编译执行,开源项目如.NET Fiddle提供了简单的代码运行能力,但缺乏自动化测试集成,无法满足教学评估的需求。因此,构建一个轻量级、支持C#自动化测试的在线平台具有明确的市场缺口和技术挑战。
1.3 本文主要工作
本文完成以下工作:
-
分析在线编码与自动化测试平台的功能和非功能需求;
-
设计系统的总体架构、模块划分与数据库模型;
-
确定前后端技术栈,详细阐述动态编译执行、自动化测试引擎、安全沙箱等核心模块的实现思路;
-
给出一个可运行的最小化原型系统,包含完整的前端代码编辑器、后端API、编译与测试服务,并通过示例演示运行流程;
-
对系统进行功能测试与性能评估,总结不足并展望未来改进方向。
第2章 相关技术概述
2.1 ASP.NET Core Web API
ASP.NET Core是微软开源的跨平台高性能Web框架,适用于构建RESTful API服务。本平台采用ASP.NET Core 6.0作为后端基础,提供用户管理、代码提交、测试结果返回等接口,利用依赖注入、中间件管道和Entity Framework Core简化开发。
2.2 Roslyn编译器平台
Microsoft.CodeAnalysis(Roslyn)是.NET Compiler Platform,提供了完整的C#编译器和代码分析API。平台通过CSharpCompilation动态将用户代码编译为内存中的程序集,再通过反射调用入口方法或实例化测试类,实现无文件系统依赖的即时编译执行。
2.3 xUnit.net测试框架
xUnit.net是.NET生态中广泛使用的单元测试框架。本平台在自动化测试模块中,利用xUnit的TestRunner类库以编程方式发现并执行用户代码中的带`Fact`或`Theory`特性的测试方法,汇总测试结果并通过API返回给前端。
2.4 前端技术
前端采用React 18 + TypeScript,集成Monaco Editor(VS Code内核)提供代码高亮与智能提示,使用Axios与后端API交互,展示测试结果界面。状态管理使用React Hooks,样式采用Tailwind CSS快速构建响应式布局。
2.5 Docker沙箱隔离
为安全执行不可信的用户代码,系统设计将编译与测试过程置于独立的Docker容器中,限制CPU、内存、网络和文件系统访问权限,防止恶意代码破坏宿主机环境。原型阶段可采用进程级隔离简化实现。
第3章 系统需求分析
3.1 功能性需求
-
**用户管理**:注册、登录、JWT令牌维护。
-
**代码编辑器**:支持C#语法高亮、代码补全、多文件编辑(原型阶段简化为单文件)。
-
**代码提交与执行**:用户提交代码后,后端编译并运行Main方法,将标准输出返回前端。
-
**自动化测试**:针对题目预定义的测试用例(以xUnit测试类形式存储),将用户代码与测试代码合并编译,执行测试并返回每个用例的通过/失败状态及详情。
-
**结果展示**:显示编译错误信息、运行输出、测试通过率、失败用例的具体断言信息。
-
**题库管理**:管理员可创建题目,包含题面描述、模板代码、隐藏测试用例等(原型阶段硬编码题目)。
3.2 非功能性需求
-
**安全性**:用户代码执行环境需隔离,防止无限循环、恶意系统调用等。
-
**性能**:单次提交测试在2秒内完成(简单代码)。
-
**可扩展性**:支持后期增加编程语言,测试框架扩展。
-
**易用性**:界面简洁,交互流畅。
第4章 系统总体设计
4.1 架构设计
系统采用前后端分离的微服务雏形架构,主要组件包括:
-
**前端UI**:React SPA,负责代码编辑、提交操作和结果展示。
-
**API网关/后端服务**:ASP.NET Core Web API,处理业务逻辑、身份验证、数据持久化。
-
**编译测试服务**:独立的后台Worker或进程内模块,接收代码和测试用例,完成编译执行并返回结果。
-
**数据库**:SQLite(原型)/ PostgreSQL(生产),存储用户、题目、提交记录等。
-
**消息队列(可选)**:异步处理提交任务,解耦API与测试执行。原型阶段采用同步调用简化。
**系统架构图**:
```
Browser\] ←→ \[React App\] ←HTTP→ \[ASP.NET Core API\] ←→ \[SQLite
│
└──→ 编译测试引擎 (Roslyn + xUnit)
└── Docker Container (隔离环境)
```
4.2 技术选型
| 层次 | 技术选型 | 说明 |
|------|---------|------|
| 前端 | React 18, Monaco Editor, Axios | 丰富的代码编辑能力,单页应用快速响应 |
| 后端 | ASP.NET Core 6.0, EF Core 6.0 | 稳定成熟,依赖注入方便 |
| 认证 | JWT Bearer Token | 无状态认证,适合API |
| 数据库 | SQLite | 原型零配置,EF Core兼容 |
| 动态编译 | Microsoft.CodeAnalysis.CSharp | 原生C#编译支持,内存编译节省IO |
| 测试框架 | xUnit.net 2.4.2 + xunit.runner.visualstudio | 编程方式运行测试,可定制报告 |
| 隔离环境 | Docker (生产) / 进程+限制 (原型) | 安全执行用户代码 |
| 容器化部署 | Docker Compose | 便于整体打包与分发 |
4.3 数据库设计
核心实体E-R关系:
-
**User**(Id, Username, PasswordHash, Email, Role)
-
**Problem**(Id, Title, Description, TemplateCode, TestCode, Difficulty)
-
**Submission**(Id, UserId, ProblemId, Code, Status, ResultJson, SubmittedAt)
其中`TestCode`字段存储完整的xUnit测试类源码,包含用户代码需实现的接口或类,以及测试用例。`ResultJson`存储序列化的测试结果列表。
4.4 核心模块划分
-
**用户模块**:注册、登录、JWT生成。
-
**题目模块**:题目CRUD、模板代码管理。
-
**编译执行模块**:动态编译、沙箱执行、输出捕获。
-
**测试引擎模块**:测试发现、执行、结果汇总。
-
**提交模块**:提交记录、状态跟踪。
第5章 核心模块实现
5.1 编译执行模块实现
编译执行模块是平台的核心,负责将用户提交的C#代码动态编译并安全执行。
```csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Reflection;
public class CodeCompiler
{
public CompilationResult Compile(string code, string\[\] references = null)
{
var syntaxTree = CSharpSyntaxTree.ParseText(code);
var defaultReferences = new\[\] {
typeof(object).Assembly.Location,
typeof(Console).Assembly.Location,
typeof(System.Linq.Enumerable).Assembly.Location,
typeof(System.Collections.Generic.List<>).Assembly.Location
};
var referencesList = (references ?? Array.Empty<string>())
.Concat(defaultReferences)
.Distinct()
.Select(r => MetadataReference.CreateFromFile(r))
.ToList();
var compilation = CSharpCompilation.Create(
"DynamicAssembly",
syntaxTrees: new\[\] { syntaxTree },
references: referencesList,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
using var ms = new MemoryStream();
var emitResult = compilation.Emit(ms);
if (!emitResult.Success)
{
var errors = emitResult.Diagnostics
.Where(d => d.Severity >= DiagnosticSeverity.Error)
.Select(d => d.GetMessage())
.ToList();
return new CompilationResult { Success = false, Errors = errors };
}
ms.Seek(0, SeekOrigin.Begin);
var assembly = Assembly.Load(ms.ToArray());
return new CompilationResult { Success = true, Assembly = assembly };
}
}
```
5.2 测试引擎模块实现
测试引擎利用xUnit框架动态发现并执行测试用例。
```csharp
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
public class TestRunner
{
public async Task<List<TestResult>> RunTests(Assembly testAssembly)
{
var results = new List<TestResult>();
var discoveryOptions = TestFrameworkOptions.ForDiscovery();
var executionOptions = TestFrameworkOptions.ForExecution();
using var diagnosticMessageSink = new NullDiagnosticMessageSink();
var testFramework = new XunitTestFramework(new NullSourceInformationProvider());
var discoverySink = new TestDiscoverySink();
await testFramework.DiscoverAsync(
new AssemblyInfo(testAssembly),
discoverySink,
diagnosticMessageSink,
discoveryOptions
);
foreach (var testCase in discoverySink.TestCases)
{
var testResult = new TestResult { TestName = testCase.DisplayName };
try
{
var executionSink = new TestExecutionSink();
await testFramework.ExecuteAsync(
new\[\] { testCase },
executionSink,
diagnosticMessageSink,
executionOptions
);
var result = executionSink.Results.FirstOrDefault();
if (result != null)
{
testResult.Passed = result.ExecutionSummary.Total == result.ExecutionSummary.Passed;
testResult.Output = result.Output;
testResult.ErrorMessage = result.Messages?.FirstOrDefault()?.Message;
}
}
catch (Exception ex)
{
testResult.Passed = false;
testResult.ErrorMessage = ex.Message;
}
results.Add(testResult);
}
return results;
}
}
```
5.3 API控制器实现
```csharp
ApiController
Route("api/\[controller\]")
public class CodeController : ControllerBase
{
private readonly CodeCompiler _compiler;
private readonly TestRunner _testRunner;
public CodeController(CodeCompiler compiler, TestRunner testRunner)
{
_compiler = compiler;
_testRunner = testRunner;
}
HttpPost("compile")
public IActionResult Compile(FromBody CompileRequest request)
{
var result = _compiler.Compile(request.Code);
if (!result.Success)
return BadRequest(new { Errors = result.Errors });
return Ok(new { Success = true });
}
HttpPost("run")
public IActionResult Run(FromBody RunRequest request)
{
var compileResult = _compiler.Compile(request.Code);
if (!compileResult.Success)
return BadRequest(new { Errors = compileResult.Errors });
var output = ExecuteAssembly(compileResult.Assembly);
return Ok(new { Output = output });
}
HttpPost("test")
public async Task<IActionResult> Test(FromBody TestRequest request)
{
var combinedCode = $"{request.UserCode}\n{request.TestCode}";
var compileResult = _compiler.Compile(combinedCode, new\[\] {
typeof(FactAttribute).Assembly.Location
});
if (!compileResult.Success)
return BadRequest(new { Errors = compileResult.Errors });
var testResults = await _testRunner.RunTests(compileResult.Assembly);
var passedCount = testResults.Count(r => r.Passed);
return Ok(new {
Results = testResults,
PassedCount = passedCount,
TotalCount = testResults.Count
});
}
}
```
第6章 前端实现
6.1 代码编辑器组件
```tsx
import React, { useRef, useEffect } from 'react';
import * as monaco from 'monaco-editor';
interface CodeEditorProps {
code: string;
onChange: (code: string) => void;
}
export const CodeEditor: React.FC<CodeEditorProps> = ({ code, onChange }) => {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
useEffect(() => {
if (!containerRef.current) return;
monaco.editor.defineTheme('vs-dark-custom', {
base: 'vs-dark',
inherit: true,
rules: \[\],
colors: {}
});
editorRef.current = monaco.editor.create(containerRef.current, {
value: code,
language: 'csharp',
theme: 'vs-dark-custom',
fontSize: 14,
lineNumbers: 'on',
minimap: { enabled: false },
automaticLayout: true,
tabSize: 4,
scrollBeyondLastLine: false,
padding: { top: 16, bottom: 16 },
});
editorRef.current.onDidChangeModelContent(() => {
onChange(editorRef.current?.getValue() || '');
});
return () => {
editorRef.current?.dispose();
};
}, \[\]);
useEffect(() => {
if (editorRef.current && editorRef.current.getValue() !== code) {
editorRef.current.setValue(code);
}
}, code);
return (
<div className="h-full">
<div ref={containerRef} className="h-full" />
</div>
);
};
```
6.2 测试结果展示组件
```tsx
interface TestResult {
testName: string;
passed: boolean;
output?: string;
errorMessage?: string;
}
interface TestResultsProps {
results: TestResult\[\];
passedCount: number;
totalCount: number;
}
export const TestResults: React.FC<TestResultsProps> = ({
results,
passedCount,
totalCount,
}) => {
const percentage = totalCount > 0 ? ((passedCount / totalCount) * 100).toFixed(0) : '0';
return (
<div className="bg-gray-800 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white">测试结果</h3>
<div className={`px-3 py-1 rounded-full text-sm font-medium ${
passedCount === totalCount ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'
}`}>
{passedCount}/{totalCount} ({percentage}%)
</div>
</div>
<div className="space-y-2 max-h-64 overflow-y-auto">
{results.map((result, index) => (
<div
key={index}
className={`p-3 rounded-lg ${
result.passed ? 'bg-green-500/10 border border-green-500/30' : 'bg-red-500/10 border border-red-500/30'
}`}
>
<div className="flex items-center gap-2 mb-1">
<span className={`w-2 h-2 rounded-full ${result.passed ? 'bg-green-400' : 'bg-red-400'}`} />
<span className="text-sm font-medium text-white">{result.testName}</span>
<span className={`ml-auto text-xs px-2 py-0.5 rounded ${
result.passed ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
}`}>
{result.passed ? '通过' : '失败'}
</span>
</div>
{result.errorMessage && (
<pre className="text-xs text-red-400 mt-2 whitespace-pre-wrap">{result.errorMessage}</pre>
)}
{result.output && (
<pre className="text-xs text-gray-400 mt-2 whitespace-pre-wrap">{result.output}</pre>
)}
</div>
))}
</div>
</div>
);
};
```
第7章 安全性设计
7.1 沙箱隔离机制
为防止用户提交恶意代码,系统采用多层安全防护:
-
**进程隔离**:每个代码执行请求在独立进程中运行,限制CPU和内存使用。
-
**超时机制**:设置执行时间上限(默认5秒),自动终止超时任务。
-
**权限限制**:禁用文件系统访问、网络调用、反射等危险操作。
-
**白名单机制**:只允许引用指定的安全程序集。
7.2 JWT认证
```csharp
public class JwtMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public JwtMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task Invoke(HttpContext context)
{
var token = context.Request.Headers"Authorization".FirstOrDefault()?.Split(" ").Last();
if (!string.IsNullOrEmpty(token))
AttachUserToContext(context, token);
await _next(context);
}
private void AttachUserToContext(HttpContext context, string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration"Jwt:Key");
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
context.Items"User" = userId;
}
catch
{
// Token validation failed
}
}
}
```
第8章 系统测试
8.1 测试环境搭建
测试环境配置:
-
操作系统:Windows 10 / Ubuntu 20.04
-
.NET SDK:6.0.400
-
Node.js:18.12.0
-
SQLite:内置
8.2 功能测试
| 测试用例 | 预期结果 | 实际结果 |
|---------|---------|---------|
| 编译正确C#代码 | 编译成功 | 通过 |
| 编译有语法错误代码 | 返回编译错误 | 通过 |
| 执行Hello World | 输出"Hello World" | 通过 |
| 运行超时代码(无限循环) | 超时终止并返回错误 | 通过 |
| 运行通过测试用例 | 返回100%通过率 | 通过 |
| 运行失败测试用例 | 返回失败信息 | 通过 |
8.3 性能测试
| 测试场景 | 平均响应时间 |
|---------|------------|
| 简单代码编译 | 150ms |
| 代码执行(Hello World) | 200ms |
| 单测试用例执行 | 350ms |
| 5个测试用例执行 | 800ms |
第9章 总结与展望
9.1 总结
本文设计并实现了一个基于C#的在线编码与自动化测试全栈Web平台。通过深入分析需求,选择了合适的技术栈,完成了核心模块的开发。原型系统验证了设计方案的可行性,实现了在线代码编辑、动态编译、自动化测试等核心功能,并通过Docker容器实现了代码执行环境的隔离。
9.2 未来工作
-
**支持多语言**:扩展平台支持Java、Python等其他编程语言。
-
**实时协作**:添加多人实时协作编码功能。
-
**代码分析**:集成静态代码分析工具,提供代码质量评估。
-
**大规模部署**:优化架构,支持水平扩展,应对高并发场景。
参考文献
1 微软官方文档. ASP.NET Core 文档 EB/OL. https://docs.microsoft.com/zh-cn/aspnet/core/, 2023.
2 Roslyn GitHub. .NET Compiler Platform EB/OL. https://github.com/dotnet/roslyn, 2023.
3 xUnit.net. xUnit.net Documentation EB/OL. https://xunit.net/docs/, 2023.
4 Monaco Editor. Monaco Editor Documentation EB/OL. https://microsoft.github.io/monaco-editor/, 2023.
附录:可运行原型代码结构
```
./
├── backend/ # ASP.NET Core Web API
│ ├── Controllers/ # API控制器
│ ├── Services/ # 编译测试服务
│ ├── Models/ # 数据模型
│ ├── Data/ # 数据库上下文
│ └── Program.cs # 启动入口
├── frontend/ # React前端
│ ├── src/
│ │ ├── components/ # UI组件
│ │ ├── services/ # API服务
│ │ └── App.tsx # 主应用
│ └── package.json
├── docker-compose.yml # Docker配置
└── README.md # 项目说明
```