我如何启动任何.NET Core Web API项目
2022/2/1 6:59:56
本文主要是介绍我如何启动任何.NET Core Web API项目,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
目录
介绍
步骤
在Visual Studio中创建Web API项目
选项1
选项 2
IIS配置
配置Swashbuckle/Swagger
添加Swashbuckle
添加Newtonsoft.Json
添加异常服务
设置CORS策略
添加身份验证服务
读取appsettings.json
(可选)添加数据库上下文
可选地添加RestSharp
(可选)格式化JSON
添加更多基本测试端点
认证测试
其他注意事项
一般文件夹结构
单例、范围和瞬态服务生命周期
服务还是控制器即服务?
结论
- 下载演示 - 8.6 KB
介绍
本文介绍了我在.NET Core 3.1中创建样板Web API所经历的典型过程。从目录中,您可以看到我设置的所有不同部分。
步骤
在Visual Studio中创建Web API项目
选项1
上面的屏幕截图是当您右键单击Visual Studio 2019并选择“以管理员身份运行”时得到的结果,您可能无论如何都必须这样做以调试Web API项目。这将向您显示模板列表,您将选择ASP.NET Core Web API:
最终屏幕(带有“创建”按钮)具有以下选项:
- 我不选择身份验证类型,因为我编写的大多数Web API使用自定义身份验证例程,这将在下面讨论。
- 我也没有为HTTPS配置Web API,因为Web API将在IIS下运行,IIS已经在生产环境中为HTTPS配置。
选项 2
或者,在VS2019中,选择File => New Project,然后选择“Installed ”和“ASP.NET Core Web Application”:
然后你会看到一些选项,包括“API ”:
奇怪的是,这种方法不会提示您输入身份验证类型和“为HTTPS配置”。
IIS配置
右键单击解决方案,然后从弹出菜单中单击“属性”:
创建一个名为“IIS ”的新配置文件并选择IIS作为启动选项:
你现在应该看到:
更改选项以使用“weatherforecast ”页面启动浏览器:
验证是否选中匿名身份验证并且未选中Windows身份验证:
重要的!我发现我还必须编辑launchSettings.json文件,完全删除对iisExpress的引用,否则Visual Studio会继续通过IIS Express启动Web API,所以这就是我的launchSettings.json文件现在的样子:
{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iis": { "applicationUrl": "http://localhost/Demo", "sslPort": 0 } }, "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "IIS": { "commandName": "IIS", "launchBrowser": true, "launchUrl": "weatherforecast", "sqlDebugging": true } } }
这是为什么,我不知道!
您现在可以运行Visual Studio为您创建的样板WeatherForecast Web API,当然,我们将删除它:
Visual Studio将自动为您提供IIS——我真的很喜欢这个功能!
配置Swashbuckle/Swagger
在项目的构建选项中:
启用XML文档。
另外,忽略警告1591:
否则IDE会不断警告您缺少XML注释。
添加Swashbuckle
Swashbuckle可用于生成描述所有API端点的页面以及测试这些端点的笨拙方法。尽管如此,我发现它特别有用,因为其他开发人员几乎总是使用这些API编写前端代码。还可以为每个API添加属性标签,并使用Swashbuckle提供的各种前端“调用API”实用程序之一来自动生成前端方法。但是请注意,其中一些实用程序会生成看起来非常奇怪的代码。
包括Swashbuckle文档非常简单。右键单击项目依赖项并选择管理NuGet包:
浏览“Swash”:
并安装包“Swashbuckle.AspNetCore”(版本可能大于6.1.4):
在Startup.cs文件中,将Swagger服务添加到ConfigureServices方法中,如下所示:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // ADD THIS: services.AddSwaggerGen(c => { var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); }); }
在该Configure方法中,将app.UseRouting();后的代码添加到应用程序构建器中:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); // ------ ADD THIS ------ app.UseSwagger() .UseSwaggerUI(c => { c.SwaggerEndpoint("/demo/swagger/v1/swagger.json", "Demo API V1"); }); // ====================== app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
请注意,端点路径中的第一个元素与应用程序名称“demo”相同。
当您启动应用程序时,您现在可以导航到Demo/swagger(或您为项目命名的任何名称,您将看到:
添加Newtonsoft.Json
我更喜欢Newtonsoft.Json,因为在我看来,它比.NET Core内置的默认JSON序列化器要好。一方面,它处理序列化/反序列化枚举,维护基于模型属性的大小写,处理没有任何特殊代码的自引用循环,等等。我还喜欢对返回的JSON进行漂亮的格式化,因为它更易于阅读以用于调试目的。同样,在NuGet包管理器中,安装Microsoft.AspNetCore.Mvc.Newtonsoft.JSON,确保选择最新的3.1.x版本:
在Startup.cs中,修改ConfigureServices方法以指示其使用NewtonsoftJson:
public void ConfigureServices(IServiceCollection services) { services.AddControllers() // must be version 3.1.13 -- version 5's support .NET 5 only. // <a href="https://anthonygiretti.com/2020/05/10/why-model-binding-to-jobject- // from-a-request-doesnt-work-anymore-in-asp-net-core-3-1-and-whats-the-alternative/"> // https://anthonygiretti.com/2020/05/10/why-model-binding-to-jobject-from-a-request- // doesnt-work-anymore-in-asp-net-core-3-1-and-whats-the-alternative/</a> .AddNewtonsoftJson(); services.AddSwaggerGen(c => { var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); }); }
显然,如果您愿意,可以省略我的注释。
添加异常服务
特别是在调试时,我不只是想要500 Internal Server Error,我更喜欢一致且格式良好的响应,指示错误和堆栈跟踪。
在项目属性的Debug部分,添加ASPNETCORE_ENVIRONMENT值为Development的环境变量:
根据这篇文章,ASP.NET Core使用该ASPNETCORE_ENVIRONMENT变量来确定当前环境。
添加一个MiddlewareExceptionHandler,我通常在文件夹Services中的文件ExceptionService.cs中执行此操作:
这是代码:
// Borrowed from here: http://netitude.bc3tech.net/2017/07/31/ // using-middleware-to-trap-exceptions-in-asp-net-core/ // Note that middleware exception handling is different from exception filters: // https://damienbod.com/2015/09/30/asp-net-5-exception-filters-and-resource-filters/ // https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters? // view=aspnetcore-2.2#exception-filters // Exception filters do NOT catch exceptions that occur in the middleware. public class MiddlewareExceptionHandler { private readonly RequestDelegate _next; public MiddlewareExceptionHandler(RequestDelegate next) { _next = next ?? throw new ArgumentNullException(nameof(next)); } public async Task Invoke(HttpContext context) { try { await _next(context); } // This handles the problem when the AUTHORIZATION token doesn't // actually validate and ASP.NET Core middleware generates this: // An unhandled exception occurred while processing the request. // InvalidOperationException: No authenticationScheme was specified, // and there was no DefaultChallengeScheme found. // We want to handle this error as a "not authorized" response. catch (InvalidOperationException) { if (context.Response.HasStarted) { throw; } context.Response.Clear(); context.Response.StatusCode = 401; context.Response.ContentType = "application/json"; await context.Response.WriteAsync("{\"status\":401,\"message\":\"Not authorized.\"}"); } catch (Exception ex) { if (context.Response.HasStarted) { throw; } context.Response.Clear(); context.Response.StatusCode = 500; context.Response.ContentType = "application/json"; var exReport = new ExceptionReport(ex); var exJson = JsonConvert.SerializeObject(exReport, Formatting.Indented); await context.Response.WriteAsync(exJson); } } } // Extension method used to add the middleware to the HTTP request pipeline. public static class MiddlewareExceptionExtensions { public static IApplicationBuilder UseHttpStatusCodeExceptionMiddleware (this IApplicationBuilder builder) { return builder.UseMiddleware<MiddlewareExceptionHandler>(); } }
同样在该文件中的“异常报告”代码的其余部分是:
public static class ExceptionReportExtensionMethods { public static ExceptionReport CreateReport(this Exception ex) { return new ExceptionReport(ex); } public static T[] Drop<T>(this T[] items, int n = 0) { // We could use C# 8's ^ operator to take all but the last n... return items.Take(items.Length - (1 + n)).ToArray(); } } public class ExceptionReport { public DateTime When { get; } = DateTime.Now; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string ApplicationMessage { get; set; } public string ExceptionMessage { get; set; } public List<StackFrameData> CallStack { get; set; } = new List<StackFrameData>(); [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ExceptionReport InnerException { get; set; } public ExceptionReport(Exception ex, int exceptLastN = 0) { ExceptionMessage = ex.Message; var st = new StackTrace(ex, true); var frames = st.GetFrames()?.Drop(exceptLastN) ?? new StackFrame[0]; CallStack.AddRange( frames .Where(frame => !String.IsNullOrEmpty(frame.GetFileName())) .Select(frame => new StackFrameData(frame))); InnerException = ex.InnerException?.CreateReport(); } } public class StackFrameData { public string FileName { get; private set; } public string Method { get; private set; } public int LineNumber { get; private set; } public StackFrameData(StackFrame sf) { FileName = sf.GetFileName(); Method = sf.GetMethod().Name; LineNumber = sf.GetFileLineNumber(); } public override string ToString() { return $"File: {FileName}\r\nMethod: {Method}\r\nLine: {LineNumber}"; } }
然后,在Startup.cs中,修改Configure方法if env.IsDevelopment()中的处理方式:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { // Do not halt execution. I don't fully understand this. // See http://netitude.bc3tech.net/2017/07/31/ // using-middleware-to-trap-exceptions-in-asp-net-core/ // "Notice the difference in order when in development mode vs not. // This is important as the Developer Exception page // passes through the exception to our handler so in order to get the // best of both worlds, you want the Developer Page handler first. // In production, however, since the default Exception Page halts execution, // we definitely to not want that one first." app.UseDeveloperExceptionPage(); app.UseHttpStatusCodeExceptionMiddleware(); } else { app.UseHttpStatusCodeExceptionMiddleware(); app.UseExceptionHandler("/Home/Error"); } ...
因为我处于开发模式,所以我没有实现错误页面。
通过在WeatherForecastController.cs文件中添加一个抛出异常的GET方法来测试异常处理:
[HttpGet("TestException")] public void TestException() { throw new Exception("Exception occurred!"); }
运行应用程序并导航到http://localhost/Demo/weatherForecast/testException,您应该看到:
如果异常发生在代码内部的某个地方,您会看到更多的堆栈跟踪。
设置CORS策略
我们通常希望启用某种CORS(跨源资源共享)策略(在此处阅读更多信息),尽管对于大多数应用程序,我将其设置为任何源。在Startup.cs文件中的ConfigureServices方法中,添加以下内容:
services.AddCors(options => options.AddDefaultPolicy(builder => builder .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials() // Needed because we can't use AllowAnyOrigin with AllowCredentials // https://jasonwatmore.com/post/2020/05/20/ // aspnet-core-api-allow-cors-requests-from-any-origin-and-with-credentials // https://docs.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-5.0 .SetIsOriginAllowed(origin => true) .WithExposedHeaders(EXPOSED_HEADERS) ) );
在Configure方法中,在UseRouting()之后,添加对UseCors()的调用:
app .UseRouting() .UseCors();
设置CORS是一件非常非常善变的事情。我最终处理的大多数生产问题都与CORS的挑剔性质有关,所以要小心。如果某个端点失败,则使用浏览器的调试器来确定飞行前的CORS测试是否未能确定是端点问题还是CORS问题。
添加身份验证服务
大多数API需要身份验证(也可能需要授权,但我通常不实现用户角色授权。)为了添加身份验证,我使用以下模板在Services文件夹中创建了一个AuthenticationService.cs文件:
public class TokenAuthenticationSchemeOptions : AuthenticationSchemeOptions { } public class AuthenticationService : AuthenticationHandler<TokenAuthenticationSchemeOptions> { private SessionService sessionService; public AuthenticationService( IOptionsMonitor<TokenAuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { this.sessionService = sessionService; } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { Task<AuthenticateResult> result = Task.FromResult(AuthenticateResult.Fail("Not authorized.")); // Authentication confirms that users are who they say they are. // Authorization gives those users permission to access a resource. if (Request.Headers.ContainsKey("yourAuthKey")) { // Verify the key... // If verified, optionally add some claims about the user... var claims = new[] { new Claim("[key]", "value"), }; // Generate claimsIdentity on the name of the class: var claimsIdentity = new ClaimsIdentity(claims, nameof(AuthenticationService)); // Generate AuthenticationTicket from the Identity // and current authentication scheme. var ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name); result = Task.FromResult(AuthenticateResult.Success(ticket)); } return result; } }
在构造函数中,您可能还希望传入某种“帐户服务”——一种允许您连接到数据库以验证用户帐户的服务。
此外,在Startup.cs文件中,将身份验证方案添加到ConfigureServices方法中:
services .AddAuthentication("tokenAuth") .AddScheme<TokenAuthenticationSchemeOptions, AuthenticationService>("tokenAuth", ops => { });
鉴于上述代码将无法通过身份验证,除非我们提供带有密钥“yourAuthKey”的标头,我们可以使用API端点对其进行测试(见下文)。
最后,添加UseAuthentication()到Configure方法中:
app .UseAuthentication() .UseRouting() .UseCors();
读取appsettings.json
您可以在此处阅读ASP.NET Core中的配置。出于我们的目的,我只是要设置一个简单的配置文件。演示中的配置文件有applications.json和appsettings.Development.json,与ASPNETCORE_ENVIRONMENT环境变量值相关联。包含环境值的appsettings会覆盖applications.json中的内容。例如,在appsetting.json中,我将定义几个设置,Key1和Key2:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", // I've added these keys: "Key1": "first key", "Key2": "second key" }
在appsettings.Development.json中,我将覆盖Key2:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, // I'm overriding Key2: "Key2": "Key2 override" }
为了将这些值放入一个对象中,这样我们就不会使用硬编码字符串来引用配置设置,例如Configuration("Key1"),我们可以使用感兴趣的配置键创建一个类并将该类绑定到配置。例如,给定:
public class AppSettings { public static AppSettings Settings { get; set; } public AppSettings() { Settings = this; } public string Key1 { get; set; } public string Key2 { get; set; } }
然后我们在Startup构造函数中进行绑定。
public AppSettings AppSettings { get; } = new AppSettings(); public Startup(IConfiguration configuration) { Configuration = configuration; Configuration.Bind(AppSettings); }
并且因为构造函数设置了public static变量Settings,我们可以在应用程序的任何地方访问设置。例如,我们可以创建一个返回应用程序设置的端点(有关添加更多基本测试端点的信息,请参见下文):
[AllowAnonymous] [HttpGet("AppSettings")] public object GetAppSettings() { return AppSettings.Settings; }
我们看到:
重要的!通常,您不希望公开您的应用设置,因为它们可能包含敏感信息,例如数据库密码等。
(可选)添加数据库上下文
大多数Web API需要连接到数据库,并且由于我只使用SQL Server,因此我将添加一行或多行来注册数据库上下文,具体取决于我拥有的上下文数量:
services.AddDbContext<MyDbContext>(options => options.UseSqlServer("myConnectionString"));
您通常不会对连接字符串进行硬编码,而是根据环境从应用程序设置中获取它们(见下文)。此外,这需要添加适当的数据库支持,例如Microsoft.EntityFrameworkCore.SqlServer。然后可以将DB上下文添加到服务的构造函数中:
public SomeService(MyDbContext context)
.NET Core将处理实例创建和依赖注入。
可选地添加RestSharp
如果您的服务调用其他API,我更喜欢使用RestSharp。再次在NuGet包管理器中,添加最新版本的RestSharp:
(可选)格式化JSON
如果我们修改注册NewtonsoftJson方式:
services.AddControllers() .AddNewtonsoftJson(options => options.SerializerSettings.Formatting = Formatting.Indented);
我们可以对返回的JSO全局添加格式,例如:
当我不使用Postman(它自己格式化)来调试端点时,我发现这特别有用。
添加更多基本测试端点
虽然我们之前添加了一个ExceptionTest API,但我真的不希望在“演示”控制器中使用它。同样用于添加版本端点。因此,这两个端点被添加到“public”控制器中,因为我通常不打算对它们进行身份验证:
[ApiController] [Route("[controller]")] public class Public : ControllerBase { [AllowAnonymous] [HttpGet("Version")] public object Version() { return new { Version = "1.00" }; } [AllowAnonymous] [HttpGet("TestException")] public void TestException() { throw new Exception("Exception occurred!"); } }
请注意,我有这个类派生自ControllerBase,因此我们可以利用常见的响应,例如Ok()。
对于Version API,您现在将看到:
请注意该AllowAnonymous属性,因为我们现在在其他地方使用身份验证。并在Version API端点中执行任何您喜欢的操作来获取版本。例如,我经常添加一个检查,以确保必要的DB连接也成功。
认证测试
让我们测试身份验证服务——是的,我将此端点放在“public”控制器中:
[Authorize] [HttpGet("TestAuthentication")] public ActionResult TestAuthentication() { return Ok(); }
我们看到:
如果我们使用Postman调用带有"yourAuthKey"值集的端点:
curl --location --request GET 'http://localhost/Demo/public/testauthentication' \ --header 'yourAuthKey: Somevalue'
我们看到端点返回OK:
其他注意事项
一般文件夹结构
我倾向于设置这个文件夹结构:
- Common——常见类,例如辅助类
- 控制器——应用程序支持的所有控制器。如果特定的控制器文件变大,我将为控制器创建一个文件夹,其中包含该控制器的partial类来组织端点。
- 扩展——我在我的代码中使用了多种扩展方法,这些文件放在这个文件夹中。
- 接口——我需要的任何接口定义(见下文)
- 模型——数据库上下文和模型
- 请求——定义传入请求的类
- 响应——定义传出响应的类
- 服务——我需要的任何服务(见下文)
服务接口还是具体的服务类?
我在其他人的代码中看到的常见模式之一是在注册服务时过度使用接口。例如:
services.AddSingleton<IUserCacheService, UserCacheService>();
结果是每个服务都实现了一个接口,控制器或服务构造函数中的依赖注入依赖于接口而不是具体的服务。例如:
public SomeControllerOrService(IUserCacheService userCacheService)
接口的重点是抽象实现。如果您知道实现永远不会被抽象,那么添加接口绝对没有意义——它只会混淆代码,创建更多类型,并且在更新具体服务的方法时需要维护其他东西。这绝对是毫无意义的——随着项目的增长和越来越多的服务的添加(通常只是为了定义逻辑边界),接口文件的数量也在增长,并且本质上变成了代码异味。可以改写:
services.AddSingleton<CacheServices.UsersCacheService>();
和:
public SomeControllerOrService(UserCacheService userCacheService)
将服务实现为接口的论据之一是模拟服务以测试控制器。虽然这听起来很合理,但我认为推理是不充分的。大多数(如果不是全部)业务规则和通用代码逻辑不在控制器端点代码中,而是在服务中——事实上,拥有一个或多个业务规则服务来将控制器和其他服务与控制器和其他服务解耦通常是有意义的。特定于应用程序的逻辑。鉴于此,当我编写集成测试时,我不想针对模拟服务进行测试,而是针对实际服务进行测试!像任何事情一样,存在合理的例外情况,例如当服务与其他组件(数据库、第三方 API 等)交互时,这些组件仅在“实时”登台或生产环境中可用。好的,使用接口实现服务以便它可以被模拟是有道理的,但需要注意任何业务逻辑都是在服务之外实现的。因此,请考虑服务是否会被抽象,如果您有理由(或非常)确定它不会被抽象,那么就不要为服务的接口而烦恼。
单例、范围和瞬态服务生命周期
有一个在不同类型的服务的一个很好的写了这里。总之:
- Singleton:“单例范围在第一次请求时创建服务的单个实例。之后对于每个后续请求,它将使用相同的实例。新请求不会创建服务的新实例而重用现有实例。”
- Scoped:“具有范围生命周期的服务每个请求(范围)只创建一次,即它为每个请求创建一个新实例并在该请求中重用该实例。” 例如,如果您的控制器需要一个服务,并且您的控制器中的一些其他代码引用另一个服务,也需要该服务,那么范围服务将被重用,因为它是同一请求的一部分。
- Transient:“瞬态服务总是创建一个新实例,每次我们请求它时。” 与作用域服务相反,请求期间其他服务对瞬态服务的任何要求都将获得新实例。
为什么我们有这些类型的服务生命周期?主要的答案是某些服务可能很少使用并且可能会占用大量内存,尽管我认为如果是这种情况,则您没有设计良好的服务。无论如何,您可能不希望服务在请求的生命周期之外停留,因此单例服务可能不合适。我真的想不出瞬态服务的用例(你能吗?),它们消耗内存(虽然生命周期很短),但还需要依赖注入系统为每个引用的瞬态服务创建一个新实例,这减少了应用程序表现。我通常只使用单例服务,因为我的服务不包含静态变量或状态信息。如果我需要在请求中保持状态,那么一个限定范围的服务将是正确的服务。
正如上面提到的文章所指出的,必须小心混合不同生命周期的服务。如果单例服务持久化一个作用域或瞬态服务,那么这个作用域或瞬态服务将被持久化,从而违背了“请求或每次访问期间的生命周期”的目的。
服务还是控制器即服务?
另一个考虑是,“我需要服务还是可以将代码放入控制器中?” 例如,版本是否需要服务?
[AllowAnonymous] [HttpGet("Version")] public object Version() { return new { Version = "1.00" }; }
我应该不会!它可以访问服务,例如验证数据库连接。我倾向于编写以下任一端点:
- 依赖其他服务的简单操作/响应
- 包含基于请求或数据库中数据状态的业务规则的操作
后者是更容易解决的问题。业务规则是否应该与控制器分离并放入服务或其他“业务规则”容器中?如果端点执行大量操作(有规则或没有规则),是否应该将其解耦为工作流服务?请求验证应该属于哪里?业务规则、工作流和验证是否应该根据其他应用程序配置设置或运行时用户选项进行抽象?当人们注意到端点中的代码变得相当冗长时,这些是人们应该问自己的问题。
结论
这就是我创建ASP.NET Web API项目时所经历的典型过程。我希望您在创建自己的Web API项目时会发现这很有用!
https://www.codeproject.com/Articles/5309416/How-I-Start-any-NET-Core-Web-API-Project
这篇关于我如何启动任何.NET Core Web API项目的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-03-01沐雪多租宝商城源码从.NetCore3.1升级到.Net6的步骤
- 2024-05-08首个适配Visual Studio平台的国产智能编程助手CodeGeeX正式上线!C#程序员必备效率神器!
- 2024-03-30C#设计模式之十六迭代器模式(Iterator Pattern)【行为型】
- 2024-03-29c# datetime tryparse
- 2024-02-21list find index c#
- 2024-01-24convert toint32 c#
- 2024-01-24Advanced .Net Debugging 1:你必须知道的调试工具
- 2024-01-24.NET集成IdGenerator生成分布式全局唯一ID
- 2024-01-23用CI/CD工具Vela部署Elasticsearch + C# 如何使用
- 2024-01-23.NET开源的简单、快速、强大的前后端分离后台权限管理系统