基于C#的在线编码与自动化测试全栈Web平台的设计与实现

基于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 本文主要工作

本文完成以下工作:

  1. 分析在线编码与自动化测试平台的功能和非功能需求;

  2. 设计系统的总体架构、模块划分与数据库模型;

  3. 确定前后端技术栈,详细阐述动态编译执行、自动化测试引擎、安全沙箱等核心模块的实现思路;

  4. 给出一个可运行的最小化原型系统,包含完整的前端代码编辑器、后端API、编译与测试服务,并通过示例演示运行流程;

  5. 对系统进行功能测试与性能评估,总结不足并展望未来改进方向。


第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 功能性需求

  1. **用户管理**:注册、登录、JWT令牌维护。

  2. **代码编辑器**:支持C#语法高亮、代码补全、多文件编辑(原型阶段简化为单文件)。

  3. **代码提交与执行**:用户提交代码后,后端编译并运行Main方法,将标准输出返回前端。

  4. **自动化测试**:针对题目预定义的测试用例(以xUnit测试类形式存储),将用户代码与测试代码合并编译,执行测试并返回每个用例的通过/失败状态及详情。

  5. **结果展示**:显示编译错误信息、运行输出、测试通过率、失败用例的具体断言信息。

  6. **题库管理**:管理员可创建题目,包含题面描述、模板代码、隐藏测试用例等(原型阶段硬编码题目)。

3.2 非功能性需求

  1. **安全性**:用户代码执行环境需隔离,防止无限循环、恶意系统调用等。

  2. **性能**:单次提交测试在2秒内完成(简单代码)。

  3. **可扩展性**:支持后期增加编程语言,测试框架扩展。

  4. **易用性**:界面简洁,交互流畅。


第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 核心模块划分

  1. **用户模块**:注册、登录、JWT生成。

  2. **题目模块**:题目CRUD、模板代码管理。

  3. **编译执行模块**:动态编译、沙箱执行、输出捕获。

  4. **测试引擎模块**:测试发现、执行、结果汇总。

  5. **提交模块**:提交记录、状态跟踪。


第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 沙箱隔离机制

为防止用户提交恶意代码,系统采用多层安全防护:

  1. **进程隔离**:每个代码执行请求在独立进程中运行,限制CPU和内存使用。

  2. **超时机制**:设置执行时间上限(默认5秒),自动终止超时任务。

  3. **权限限制**:禁用文件系统访问、网络调用、反射等危险操作。

  4. **白名单机制**:只允许引用指定的安全程序集。

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 未来工作

  1. **支持多语言**:扩展平台支持Java、Python等其他编程语言。

  2. **实时协作**:添加多人实时协作编码功能。

  3. **代码分析**:集成静态代码分析工具,提供代码质量评估。

  4. **大规模部署**:优化架构,支持水平扩展,应对高并发场景。


参考文献

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 # 项目说明

```

相关推荐
Raink老师1 小时前
【AI面试临阵磨枪-98】前端如何展示多模态流式输出:文字打字机 + 图片渐进 + 音频播放?
前端·人工智能·面试
牛油果子哥q1 小时前
C++六大默认成员函数深度精讲:构造/析构/拷贝/赋值/移动构造/移动赋值、编译器生成规则、深浅拷贝终极坑点与工程实战
开发语言·c++
AI_零食1 小时前
奶茶大数据运维表 - 鸿蒙PC Electron框架技术实现详解
运维·前端·华为·electron·开源·harmonyos·鸿蒙
Shadow(⊙o⊙)1 小时前
System V共享内存详解,shm系列接口,三种共享内存删除机制。System V通信缺点分析
linux·运维·服务器·开发语言·网络·c++
小雨下雨的雨1 小时前
鸿蒙PC Electron框架实现流体气泡模拟器
前端·人工智能·算法·华为·electron·鸿蒙
ZC跨境爬虫1 小时前
跟着 MDN 学JavaScript day_4:如何存储你需要的信息——变量
开发语言·前端·javascript·ui·ecmascript
189228048611 小时前
NV077固态MT29F16T08ESLCHL6-QAES:C
c语言·开发语言·性能优化
小小de风呀1 小时前
de风——【从零开始学C++】(十三):优先级队列 priority_queue 全解析 & 仿函数入门
开发语言·c++
星栈独行1 小时前
10 分钟跑起第一个 Makepad 应用:先把窗口开起来
前端·程序人生·ui·rust·开源·github