From 39b28386ae6d22acb5137a768085ecbc901f6306 Mon Sep 17 00:00:00 2001 From: Huxley Deng Date: Mon, 7 Jul 2025 15:54:44 +0800 Subject: [PATCH 1/6] Implement endpoints with Services Dependency Injection. **Note: This implementation is not in minimalAPI way and not optimized, expected to be changed soon.** Add: `UserService` and its interface `IUserService`. Fix: `UserEndpoint` is now in instance class style with DI to work. Fix: change main program to work with above design. --- OptixServe.Api/Endpoints/UserEndpoint.cs | 31 +++++++++---- OptixServe.Api/Program.cs | 58 +++++++++++++++--------- OptixServe.Core/Services/UserService.cs | 25 ++++++++++ 3 files changed, 84 insertions(+), 30 deletions(-) create mode 100644 OptixServe.Core/Services/UserService.cs diff --git a/OptixServe.Api/Endpoints/UserEndpoint.cs b/OptixServe.Api/Endpoints/UserEndpoint.cs index add9286..e824663 100644 --- a/OptixServe.Api/Endpoints/UserEndpoint.cs +++ b/OptixServe.Api/Endpoints/UserEndpoint.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using OptixServe.Core.Services; using OptixServe.Api.Dtos; namespace OptixServe.Api.Endpoints; @@ -8,25 +9,37 @@ namespace OptixServe.Api.Endpoints; [JsonSerializable(typeof(IEnumerable))] public partial class UserJsonContext : JsonSerializerContext { } -public static class UserEndpoint +public class UserEndpoint { - public static IEnumerable GetUsers() + private readonly IUserService _userService; + + // 通过构造函数注入依赖 + public UserEndpoint(IUserService userService) { - return [ - new() {Id="1234", UserName = "xxx"}, - new() {Id="5678", UserName = "yyy"}, - ]; + _userService = userService; } public static void Register(WebApplication app) { var group = app.MapGroup("/users"); - group.MapGet("/", GetAllUsers); + group.MapGet("/", (UserEndpoint endpoint) => endpoint.GetAllUsers()); + group.MapGet("/{id}", (string id, UserEndpoint endpoint) => endpoint.GetUserById(id)); } - public static IResult GetAllUsers() + public IResult GetAllUsers() { - return Results.Ok(GetUsers()); + var users = _userService.GetUsers() + .Select(u => new UserDto { Id = u.Id, UserName = u.UserName }); + return Results.Ok(users); + } + + public IResult GetUserById(string id) + { + var user = _userService.GetUserById(id); + if (user == null) + return Results.NotFound(); + + return Results.Ok(new UserDto { Id = user.Id, UserName = user.UserName }); } } \ No newline at end of file diff --git a/OptixServe.Api/Program.cs b/OptixServe.Api/Program.cs index 5f29100..50db17d 100644 --- a/OptixServe.Api/Program.cs +++ b/OptixServe.Api/Program.cs @@ -1,5 +1,6 @@ using System.CommandLine; using OptixServe.Api.Endpoints; +using OptixServe.Core.Services; class Program { @@ -41,6 +42,7 @@ class Program var configFile = parseResult.GetValue(configOption); builder.AddConfigurationWithCommand(configFile); + builder.RegisterServices(); builder.RegiserJsonContext(); var app = builder.Build(); @@ -61,27 +63,6 @@ class Program /// static class ExtensionMethods { - /// - /// Registers all API endpoints - /// - /// WebApplication instance - public static void RegisterEndpoints(this WebApplication app) - { - UserEndpoint.Register(app); - } - - /// - /// Configures JSON serialization options with custom context - /// - /// WebApplicationBuilder instance - public static void RegiserJsonContext(this WebApplicationBuilder builder) - { - builder.Services.ConfigureHttpJsonOptions(options => - { - options.SerializerOptions.TypeInfoResolverChain.Add(UserJsonContext.Default); - }); - } - /// /// Adds configuration sources to the application builder /// @@ -105,4 +86,39 @@ static class ExtensionMethods builder.Configuration.AddConfiguration(configurationBuilder.Build()); } + + /// + /// Configures JSON serialization options with custom context + /// + /// WebApplicationBuilder instance + public static void RegiserJsonContext(this WebApplicationBuilder builder) + { + builder.Services.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.TypeInfoResolverChain.Add(UserJsonContext.Default); + }); + } + + /// + /// Configures services for DI + /// + /// + public static void RegisterServices(this WebApplicationBuilder builder) + { + // Application services + builder.Services.AddScoped(); + + // WebAPI Endpoint services + builder.Services.AddScoped(); + } + + /// + /// Registers all API endpoints + /// + /// WebApplication instance + public static void RegisterEndpoints(this WebApplication app) + { + UserEndpoint.Register(app); + } + } \ No newline at end of file diff --git a/OptixServe.Core/Services/UserService.cs b/OptixServe.Core/Services/UserService.cs new file mode 100644 index 0000000..8d9d292 --- /dev/null +++ b/OptixServe.Core/Services/UserService.cs @@ -0,0 +1,25 @@ +using OptixServe.Core.Models; + +namespace OptixServe.Core.Services; + +public interface IUserService +{ + IEnumerable GetUsers(); + User? GetUserById(string Id); +} + +public class UserService : IUserService +{ + public User? GetUserById(string Id) + { + throw new NotImplementedException(); + } + + public IEnumerable GetUsers() + { + return [ + new() { Id = "1234", UserName = "xxx", Password = "pass1" }, + new() { Id = "5678", UserName = "yyy", Password = "pass2" } + ]; + } +} \ No newline at end of file From dd5d55696311ed68a3b4931a0eebc1c0d9e31971 Mon Sep 17 00:00:00 2001 From: Huxley Deng Date: Mon, 7 Jul 2025 16:01:39 +0800 Subject: [PATCH 2/6] Implement endpoints in more flexible way. Fix: re-implement `UserEndpoint` as static style which works best with minimal API, simplify the DI framework. Fix: remove no needed service register in main program. --- OptixServe.Api/Endpoints/UserEndpoint.cs | 22 +++++++--------------- OptixServe.Api/Program.cs | 3 --- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/OptixServe.Api/Endpoints/UserEndpoint.cs b/OptixServe.Api/Endpoints/UserEndpoint.cs index e824663..0a76567 100644 --- a/OptixServe.Api/Endpoints/UserEndpoint.cs +++ b/OptixServe.Api/Endpoints/UserEndpoint.cs @@ -9,34 +9,26 @@ namespace OptixServe.Api.Endpoints; [JsonSerializable(typeof(IEnumerable))] public partial class UserJsonContext : JsonSerializerContext { } -public class UserEndpoint +public static class UserEndpoint { - private readonly IUserService _userService; - - // 通过构造函数注入依赖 - public UserEndpoint(IUserService userService) - { - _userService = userService; - } - public static void Register(WebApplication app) { var group = app.MapGroup("/users"); - group.MapGet("/", (UserEndpoint endpoint) => endpoint.GetAllUsers()); - group.MapGet("/{id}", (string id, UserEndpoint endpoint) => endpoint.GetUserById(id)); + group.MapGet("/", GetAllUsers); + group.MapGet("/{id}", GetUserById); } - public IResult GetAllUsers() + public static IResult GetAllUsers(IUserService userService) { - var users = _userService.GetUsers() + var users = userService.GetUsers() .Select(u => new UserDto { Id = u.Id, UserName = u.UserName }); return Results.Ok(users); } - public IResult GetUserById(string id) + public static IResult GetUserById(string id, IUserService userService) { - var user = _userService.GetUserById(id); + var user = userService.GetUserById(id); if (user == null) return Results.NotFound(); diff --git a/OptixServe.Api/Program.cs b/OptixServe.Api/Program.cs index 50db17d..5a6961c 100644 --- a/OptixServe.Api/Program.cs +++ b/OptixServe.Api/Program.cs @@ -107,9 +107,6 @@ static class ExtensionMethods { // Application services builder.Services.AddScoped(); - - // WebAPI Endpoint services - builder.Services.AddScoped(); } /// From 6fd6c9f20dd5f139142fcff382e3afb9cedd07f9 Mon Sep 17 00:00:00 2001 From: Huxley Deng Date: Mon, 7 Jul 2025 16:16:58 +0800 Subject: [PATCH 3/6] Enable API versioning with route group. Fix: the API routing is now versioned with prefix `api/v1`, aligned to OpenAPI specifications. --- OptixServe.Api/Endpoints/UserEndpoint.cs | 4 ++-- OptixServe.Api/Program.cs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/OptixServe.Api/Endpoints/UserEndpoint.cs b/OptixServe.Api/Endpoints/UserEndpoint.cs index 0a76567..10ad026 100644 --- a/OptixServe.Api/Endpoints/UserEndpoint.cs +++ b/OptixServe.Api/Endpoints/UserEndpoint.cs @@ -11,9 +11,9 @@ public partial class UserJsonContext : JsonSerializerContext { } public static class UserEndpoint { - public static void Register(WebApplication app) + public static void Register(RouteGroupBuilder parentGroup) { - var group = app.MapGroup("/users"); + var group = parentGroup.MapGroup("/users"); group.MapGet("/", GetAllUsers); group.MapGet("/{id}", GetUserById); diff --git a/OptixServe.Api/Program.cs b/OptixServe.Api/Program.cs index 5a6961c..4952dd1 100644 --- a/OptixServe.Api/Program.cs +++ b/OptixServe.Api/Program.cs @@ -46,7 +46,8 @@ class Program builder.RegiserJsonContext(); var app = builder.Build(); - app.RegisterEndpoints(); + var apiGroup = app.MapGroup("api/v1"); + ExtensionMethods.RegisterEndpoints(apiGroup); app.Run(); }); @@ -113,9 +114,9 @@ static class ExtensionMethods /// Registers all API endpoints /// /// WebApplication instance - public static void RegisterEndpoints(this WebApplication app) + public static void RegisterEndpoints(RouteGroupBuilder rootGroup) { - UserEndpoint.Register(app); + UserEndpoint.Register(rootGroup); } } \ No newline at end of file From 7cce413f799d21ef57d9aad9d77c4af912e57bb4 Mon Sep 17 00:00:00 2001 From: Huxley Deng Date: Wed, 9 Jul 2025 12:17:25 +0800 Subject: [PATCH 4/6] Add configuration type binding and implement database connection, disable NativeAOT. Add: binding setting file items to `AppSettings` class so to provide DI access as `IOptions`. Add: EF Core and DbContext to access database in services. This results in disabling NativeAOT due to poor supports for *pre-compiled query*, however many design are optimized for AOT for later re-adoption. Add: `DesignTimeDbContextFactory` to support EF Core migrations in NativeAOT. (Kept for re-enabling AOT.) Add: `DbInitializer` for ensuring database connecting in startup. Add: `ConfigurationHelper.CreateDefaultBuilder()` to read configuration files in default locations. Note this method is currently ONLY used by `DesignTimeDbContextFactory`. Refactor is expected. Add: `CommonErrorDto` for simple error message. Add: `VersionEndpoint` ONLY for debugging and testing purpose. Verylikely to be removed in the future. Other: many utilities and fixes easy to understand. Note: EF Core migrations are excluded in the early development. Not expected to be added in version control before v1.0 beta. --- OptixServe.Api/Configuration/AppSettings.cs | 25 ++++++++ .../Configuration/ConfigurationHelper.cs | 27 +++++++++ OptixServe.Api/Dtos/Error.cs | 6 ++ OptixServe.Api/Endpoints/VersionEndpoint.cs | 35 +++++++++++ OptixServe.Api/OptixServe.Api.csproj | 15 +++-- OptixServe.Api/Program.cs | 59 +++++++++++++++---- OptixServe.Api/Utilites/DatabaseHelper.cs | 34 +++++++++++ .../Utilites/DesignTimeDbContextFactory.cs | 20 +++++++ OptixServe.Api/appsettings.json | 12 +++- OptixServe.Core/Data/AppDbContext.cs | 21 +++++++ OptixServe.Core/Data/DbInitializer.cs | 11 ++++ OptixServe.Core/Models/User.cs | 9 ++- OptixServe.Core/OptixServe.Core.csproj | 4 ++ OptixServe.Core/Services/UserService.cs | 16 ++--- 14 files changed, 266 insertions(+), 28 deletions(-) create mode 100644 OptixServe.Api/Configuration/AppSettings.cs create mode 100644 OptixServe.Api/Configuration/ConfigurationHelper.cs create mode 100644 OptixServe.Api/Dtos/Error.cs create mode 100644 OptixServe.Api/Endpoints/VersionEndpoint.cs create mode 100644 OptixServe.Api/Utilites/DatabaseHelper.cs create mode 100644 OptixServe.Api/Utilites/DesignTimeDbContextFactory.cs create mode 100644 OptixServe.Core/Data/AppDbContext.cs create mode 100644 OptixServe.Core/Data/DbInitializer.cs diff --git a/OptixServe.Api/Configuration/AppSettings.cs b/OptixServe.Api/Configuration/AppSettings.cs new file mode 100644 index 0000000..751b9eb --- /dev/null +++ b/OptixServe.Api/Configuration/AppSettings.cs @@ -0,0 +1,25 @@ +namespace OptixServe.Api.Configuration; + +public record OptixServeSettings +{ + public ApiSettings? Api { get; set; } = new(); + public DatabaseSettings? Database { get; set; } = new(); +} + +public record ApiSettings +{ + public string? Listen { get; set; } = "127.0.0.1"; + public int? Port { get; set; } = 10086; +} + +public enum DatabaseType +{ + Sqlite, + MySQL +} + +public record DatabaseSettings +{ + public DatabaseType Type { get; set; } = DatabaseType.Sqlite; + public string? Host { get; set; } +} \ No newline at end of file diff --git a/OptixServe.Api/Configuration/ConfigurationHelper.cs b/OptixServe.Api/Configuration/ConfigurationHelper.cs new file mode 100644 index 0000000..4c222e3 --- /dev/null +++ b/OptixServe.Api/Configuration/ConfigurationHelper.cs @@ -0,0 +1,27 @@ +using System; + +namespace OptixServe.Api.Configuration; + +public static class ConfigurationHelper +{ + public static IConfigurationBuilder CreateDefaultBuilder() + { + var aspEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + var netEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); + // Console.WriteLine($"ASPNETCORE_ENVIRONMENT: {aspEnv}, DOTNET_ENVIRONMENT: {netEnv}"); + var env = aspEnv ?? netEnv ?? null; + + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddJsonFile("config.json", optional: true); + + if (env != null) + { + builder.AddJsonFile($"appsettings.{env}.json", optional: true) + .AddJsonFile($"config.{env}.json", optional: true); + } + + return builder; + } +} \ No newline at end of file diff --git a/OptixServe.Api/Dtos/Error.cs b/OptixServe.Api/Dtos/Error.cs new file mode 100644 index 0000000..eb1d505 --- /dev/null +++ b/OptixServe.Api/Dtos/Error.cs @@ -0,0 +1,6 @@ +namespace OptixServe.Api.Dtos; + +public record CommonErrorDto +{ + public string? Message { get; set; } +} \ No newline at end of file diff --git a/OptixServe.Api/Endpoints/VersionEndpoint.cs b/OptixServe.Api/Endpoints/VersionEndpoint.cs new file mode 100644 index 0000000..6eb01ec --- /dev/null +++ b/OptixServe.Api/Endpoints/VersionEndpoint.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; +using Microsoft.Extensions.Options; +using OptixServe.Api.Configuration; +using OptixServe.Api.Dtos; + +namespace OptixServe.Api.Endpoints; + + +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(CommonErrorDto))] +public partial class VersionJsonContext : JsonSerializerContext { } + + +/// +/// This is a endpoint ONLY FOR TEST! +/// Should not expect ANY stable behavior on it! +/// +public static class VersionEndpoint +{ + public static void Register(RouteGroupBuilder parentGroup) + { + var group = parentGroup.MapGroup("/version"); + + group.MapGet("/", () => "v1"); + group.MapGet("/test/dbconfig", (IOptions appSettings) => + { + var dbType = appSettings.Value.Database?.Type; + var dbHost = appSettings.Value.Database?.Host; + return Results.Ok(new CommonErrorDto + { + Message = $"Set up {dbType} database on {dbHost}" + }); + }); + } +} \ No newline at end of file diff --git a/OptixServe.Api/OptixServe.Api.csproj b/OptixServe.Api/OptixServe.Api.csproj index 3815467..f69bfae 100644 --- a/OptixServe.Api/OptixServe.Api.csproj +++ b/OptixServe.Api/OptixServe.Api.csproj @@ -1,11 +1,16 @@ - - + + - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + @@ -13,7 +18,7 @@ enable enable true - true + false diff --git a/OptixServe.Api/Program.cs b/OptixServe.Api/Program.cs index 4952dd1..8da4d29 100644 --- a/OptixServe.Api/Program.cs +++ b/OptixServe.Api/Program.cs @@ -1,6 +1,9 @@ using System.CommandLine; +using OptixServe.Api.Configuration; using OptixServe.Api.Endpoints; +using OptixServe.Core.Data; using OptixServe.Core.Services; +using OptixServe.Api.Utilites; class Program { @@ -46,8 +49,15 @@ class Program builder.RegiserJsonContext(); var app = builder.Build(); + + using (var scope = app.Services.CreateScope()) + { + var initializer = scope.ServiceProvider.GetRequiredService(); + initializer.Initialize(); + } + var apiGroup = app.MapGroup("api/v1"); - ExtensionMethods.RegisterEndpoints(apiGroup); + StartupHelper.RegisterEndpoints(apiGroup); app.Run(); }); @@ -62,7 +72,7 @@ class Program /// /// Contains extension methods for WebApplicationBuilder and WebApplication /// -static class ExtensionMethods +static class StartupHelper { /// /// Adds configuration sources to the application builder @@ -88,6 +98,37 @@ static class ExtensionMethods builder.Configuration.AddConfiguration(configurationBuilder.Build()); } + /// + /// Configures DbContext services + /// + /// + /// + /// + public static IServiceCollection AddAppDatabase(this IServiceCollection services, DatabaseSettings dbSettings) + { + services.AddDbContext(options => DatabaseHelper.ConfigureDbContext(options, dbSettings)); + return services; + } + + /// + /// Configures services for DI + /// + /// WebApplicationBuilder instance + public static void RegisterServices(this WebApplicationBuilder builder) + { + // Add configuration class + var optixSettigns = builder.Configuration.GetSection("OptixServe"); + var onConfigSettings = optixSettigns.Get(); + builder.Services.Configure(optixSettigns); + + // Add DBContext class + builder.Services.AddAppDatabase(onConfigSettings?.Database!); + builder.Services.AddScoped(); + + // Application services + builder.Services.AddScoped(); + } + /// /// Configures JSON serialization options with custom context /// @@ -97,26 +138,18 @@ static class ExtensionMethods builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.TypeInfoResolverChain.Add(UserJsonContext.Default); + options.SerializerOptions.TypeInfoResolverChain.Add(VersionJsonContext.Default); }); } - /// - /// Configures services for DI - /// - /// - public static void RegisterServices(this WebApplicationBuilder builder) - { - // Application services - builder.Services.AddScoped(); - } - /// /// Registers all API endpoints /// - /// WebApplication instance + /// Root RouteGroupBuilder instance public static void RegisterEndpoints(RouteGroupBuilder rootGroup) { UserEndpoint.Register(rootGroup); + VersionEndpoint.Register(rootGroup); } } \ No newline at end of file diff --git a/OptixServe.Api/Utilites/DatabaseHelper.cs b/OptixServe.Api/Utilites/DatabaseHelper.cs new file mode 100644 index 0000000..3107d66 --- /dev/null +++ b/OptixServe.Api/Utilites/DatabaseHelper.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; +using OptixServe.Api.Configuration; +using OptixServe.Core.Data; + +namespace OptixServe.Api.Utilites; + +public static class DatabaseHelper +{ + public static string BuildConnectionString(DatabaseSettings dbSettings) + { + return dbSettings.Type switch + { + DatabaseType.Sqlite => $"Data Source={dbSettings.Host ?? "optixserve.db"}", + DatabaseType.MySQL => throw new NotSupportedException("MySQL connection is not yet implemented"), + _ => throw new NotSupportedException($"Database type {dbSettings.Type} is not supported") + }; + } + + public static void ConfigureDbContext(DbContextOptionsBuilder options, DatabaseSettings dbSettings) + { + if (dbSettings?.Type == DatabaseType.Sqlite) + { + var dbPath = dbSettings.Host ?? "optixserve.db"; + var connectionString = $"Data Source={dbPath}"; + + options.UseSqlite(connectionString, b => b.MigrationsAssembly("OptixServe.Api")); + + } + else + { + throw new NotImplementedException("Only SQLite database is currently supported"); + } + } +} \ No newline at end of file diff --git a/OptixServe.Api/Utilites/DesignTimeDbContextFactory.cs b/OptixServe.Api/Utilites/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..f9d01d8 --- /dev/null +++ b/OptixServe.Api/Utilites/DesignTimeDbContextFactory.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using OptixServe.Api.Configuration; +using OptixServe.Core.Data; + +namespace OptixServe.Api.Utilites; + +public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory +{ + public AppDbContext CreateDbContext(string[] args) + { + var configuration = ConfigurationHelper.CreateDefaultBuilder().Build(); + + var dbSettings = configuration.GetSection("OptixServe:Database").Get()!; + var optionsBuilder = new DbContextOptionsBuilder(); + DatabaseHelper.ConfigureDbContext(optionsBuilder, dbSettings); + + return new AppDbContext(optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/OptixServe.Api/appsettings.json b/OptixServe.Api/appsettings.json index 4d56694..0d75ce5 100644 --- a/OptixServe.Api/appsettings.json +++ b/OptixServe.Api/appsettings.json @@ -5,5 +5,15 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "OptixServe": { + "Api": { + "Listen": "0.0.0.0", + "Port": "54321" + }, + "Database": { + "Type": "Sqlite", + "Host": "optixserve.db" + } + } } diff --git a/OptixServe.Core/Data/AppDbContext.cs b/OptixServe.Core/Data/AppDbContext.cs new file mode 100644 index 0000000..2116fc3 --- /dev/null +++ b/OptixServe.Core/Data/AppDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using OptixServe.Core.Models; + +namespace OptixServe.Core.Data; + +public class AppDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Users { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(user => + { + user.HasKey(u => u.Id); + }); + + modelBuilder.Entity().HasData([ + new() {Id = "1", UserName = "admin", Password = "admin12345"} + ]); + } +} \ No newline at end of file diff --git a/OptixServe.Core/Data/DbInitializer.cs b/OptixServe.Core/Data/DbInitializer.cs new file mode 100644 index 0000000..bec11cc --- /dev/null +++ b/OptixServe.Core/Data/DbInitializer.cs @@ -0,0 +1,11 @@ +namespace OptixServe.Core.Data; + +public class DbInitializer(AppDbContext dbContext) +{ + private readonly AppDbContext _context = dbContext; + + public void Initialize() + { + _context.Database.EnsureCreated(); + } +} \ No newline at end of file diff --git a/OptixServe.Core/Models/User.cs b/OptixServe.Core/Models/User.cs index 671e88e..3dbd0b4 100644 --- a/OptixServe.Core/Models/User.cs +++ b/OptixServe.Core/Models/User.cs @@ -1,8 +1,15 @@ namespace OptixServe.Core.Models; +public enum PrivilegeGroup +{ + Admin, + User, +} + public record User { public required string Id { get; set; } public required string UserName { get; set; } - public required string Password { get; set; } + public string? Password { get; set; } + public PrivilegeGroup PrivilegeGroup { get; set; } = PrivilegeGroup.User; } \ No newline at end of file diff --git a/OptixServe.Core/OptixServe.Core.csproj b/OptixServe.Core/OptixServe.Core.csproj index 125f4c9..354caa7 100644 --- a/OptixServe.Core/OptixServe.Core.csproj +++ b/OptixServe.Core/OptixServe.Core.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/OptixServe.Core/Services/UserService.cs b/OptixServe.Core/Services/UserService.cs index 8d9d292..01c4d7d 100644 --- a/OptixServe.Core/Services/UserService.cs +++ b/OptixServe.Core/Services/UserService.cs @@ -1,3 +1,4 @@ +using OptixServe.Core.Data; using OptixServe.Core.Models; namespace OptixServe.Core.Services; @@ -5,21 +6,20 @@ namespace OptixServe.Core.Services; public interface IUserService { IEnumerable GetUsers(); - User? GetUserById(string Id); + User? GetUserById(string id); } -public class UserService : IUserService +public class UserService(AppDbContext dbContext) : IUserService { - public User? GetUserById(string Id) + private readonly AppDbContext _dbContext = dbContext; + + public User? GetUserById(string id) { - throw new NotImplementedException(); + return _dbContext.Users.FirstOrDefault(u => u.Id == id); } public IEnumerable GetUsers() { - return [ - new() { Id = "1234", UserName = "xxx", Password = "pass1" }, - new() { Id = "5678", UserName = "yyy", Password = "pass2" } - ]; + return _dbContext.Users.AsEnumerable(); } } \ No newline at end of file From 724b1d4dae15601d66747c81e99f6536989c3363 Mon Sep 17 00:00:00 2001 From: Huxley Deng Date: Thu, 10 Jul 2025 20:08:48 +0800 Subject: [PATCH 5/6] Add JWT authentication. Add: JWT authentication in Web API. Related configuration and services are added. --- OptixServe.Api/Configuration/AppSettings.cs | 9 +++++ OptixServe.Api/Dtos/Auth.cs | 12 ++++++ OptixServe.Api/Endpoints/UserEndpoint.cs | 30 ++++++++++++-- OptixServe.Api/OptixServe.Api.csproj | 1 + OptixServe.Api/Program.cs | 26 ++++++++++++ OptixServe.Api/Services/TokenService.cs | 44 +++++++++++++++++++++ OptixServe.Api/appsettings.json | 8 +++- OptixServe.Core/Services/UserService.cs | 6 +++ 8 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 OptixServe.Api/Dtos/Auth.cs create mode 100644 OptixServe.Api/Services/TokenService.cs diff --git a/OptixServe.Api/Configuration/AppSettings.cs b/OptixServe.Api/Configuration/AppSettings.cs index 751b9eb..7a53c2f 100644 --- a/OptixServe.Api/Configuration/AppSettings.cs +++ b/OptixServe.Api/Configuration/AppSettings.cs @@ -4,6 +4,7 @@ public record OptixServeSettings { public ApiSettings? Api { get; set; } = new(); public DatabaseSettings? Database { get; set; } = new(); + public JwtSettings? Jwt { get; set; } = new(); } public record ApiSettings @@ -12,6 +13,14 @@ public record ApiSettings public int? Port { get; set; } = 10086; } +public record JwtSettings +{ + public string Secret { get; set; } = string.Empty; + public string Issuer { get; set; } = "OptixServe"; + public string Audience { get; set; } = "OptixServeUsers"; + public int TokenExpirationMinutes { get; set; } = 60; +} + public enum DatabaseType { Sqlite, diff --git a/OptixServe.Api/Dtos/Auth.cs b/OptixServe.Api/Dtos/Auth.cs new file mode 100644 index 0000000..c867991 --- /dev/null +++ b/OptixServe.Api/Dtos/Auth.cs @@ -0,0 +1,12 @@ +namespace OptixServe.Api.Dtos; + +public record LoginRequestDto +{ + public string? UserName { get; set; } + public string? Password { get; set; } +} + +public record LoginResponseDto +{ + public string? Token { get; set; } +} \ No newline at end of file diff --git a/OptixServe.Api/Endpoints/UserEndpoint.cs b/OptixServe.Api/Endpoints/UserEndpoint.cs index 10ad026..24abe86 100644 --- a/OptixServe.Api/Endpoints/UserEndpoint.cs +++ b/OptixServe.Api/Endpoints/UserEndpoint.cs @@ -1,12 +1,16 @@ using System.Text.Json.Serialization; using OptixServe.Core.Services; using OptixServe.Api.Dtos; +using OptixServe.Api.Services; +using Microsoft.AspNetCore.Authorization; namespace OptixServe.Api.Endpoints; [JsonSerializable(typeof(UserDto))] [JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(LoginRequestDto))] +[JsonSerializable(typeof(LoginResponseDto))] // For returning the token string public partial class UserJsonContext : JsonSerializerContext { } public static class UserEndpoint @@ -15,8 +19,28 @@ public static class UserEndpoint { var group = parentGroup.MapGroup("/users"); - group.MapGet("/", GetAllUsers); - group.MapGet("/{id}", GetUserById); + group.MapPost("/login", LoginUser); + group.MapGet("/", GetAllUsers).RequireAuthorization(); + group.MapGet("/{id}", GetUserById).RequireAuthorization(); + } + + public static IResult LoginUser(LoginRequestDto loginRequest, IUserService userService, ITokenService tokenService) + { + if (string.IsNullOrEmpty(loginRequest.UserName) || string.IsNullOrEmpty(loginRequest.Password)) + { + return Results.BadRequest("Username and password are required."); + } + + // Password hashing and salting will be implemented later. + var user = userService.GetUserByUsername(loginRequest.UserName); + + if (user == null || user.Password != loginRequest.Password) + { + return Results.Unauthorized(); + } + + var token = tokenService.GenerateToken(user); + return Results.Ok(new LoginResponseDto { Token = token }); } public static IResult GetAllUsers(IUserService userService) @@ -34,4 +58,4 @@ public static class UserEndpoint return Results.Ok(new UserDto { Id = user.Id, UserName = user.UserName }); } -} \ No newline at end of file +} diff --git a/OptixServe.Api/OptixServe.Api.csproj b/OptixServe.Api/OptixServe.Api.csproj index f69bfae..c3e18e4 100644 --- a/OptixServe.Api/OptixServe.Api.csproj +++ b/OptixServe.Api/OptixServe.Api.csproj @@ -5,6 +5,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OptixServe.Api/Program.cs b/OptixServe.Api/Program.cs index 8da4d29..6d25783 100644 --- a/OptixServe.Api/Program.cs +++ b/OptixServe.Api/Program.cs @@ -1,6 +1,10 @@ using System.CommandLine; +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; using OptixServe.Api.Configuration; using OptixServe.Api.Endpoints; +using OptixServe.Api.Services; using OptixServe.Core.Data; using OptixServe.Core.Services; using OptixServe.Api.Utilites; @@ -50,6 +54,9 @@ class Program var app = builder.Build(); + app.UseAuthentication(); + app.UseAuthorization(); + using (var scope = app.Services.CreateScope()) { var initializer = scope.ServiceProvider.GetRequiredService(); @@ -127,6 +134,25 @@ static class StartupHelper // Application services builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // Add Authentication and Authorization + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + var jwtSettings = onConfigSettings?.Jwt ?? throw new ArgumentNullException(nameof(builder), "JWT settings are not configured."); + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSettings.Issuer, + ValidAudience = jwtSettings.Audience, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret)) + }; + }); + builder.Services.AddAuthorization(); } /// diff --git a/OptixServe.Api/Services/TokenService.cs b/OptixServe.Api/Services/TokenService.cs new file mode 100644 index 0000000..f124f92 --- /dev/null +++ b/OptixServe.Api/Services/TokenService.cs @@ -0,0 +1,44 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using OptixServe.Api.Configuration; +using OptixServe.Core.Models; + +namespace OptixServe.Api.Services; + +public interface ITokenService +{ + public string GenerateToken(User user); +} + +public class TokenService(IOptions optixServeSettings) : ITokenService +{ + private readonly JwtSettings _jwtSettings = optixServeSettings.Value.Jwt ?? throw new ArgumentNullException(nameof(optixServeSettings), "JWT settings are not configured."); + + public string GenerateToken(User user) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_jwtSettings.Secret); + + var claims = new List + { + new (ClaimTypes.NameIdentifier, user.Id.ToString()), + new (ClaimTypes.Name, user.UserName) + // Add roles if applicable: new Claim(ClaimTypes.Role, user.Role) + }; + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Expires = DateTime.UtcNow.AddMinutes(_jwtSettings.TokenExpirationMinutes), + Issuer = _jwtSettings.Issuer, + Audience = _jwtSettings.Audience, + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } +} \ No newline at end of file diff --git a/OptixServe.Api/appsettings.json b/OptixServe.Api/appsettings.json index 0d75ce5..e4a86f3 100644 --- a/OptixServe.Api/appsettings.json +++ b/OptixServe.Api/appsettings.json @@ -14,6 +14,12 @@ "Database": { "Type": "Sqlite", "Host": "optixserve.db" + }, + "Jwt": { + "Secret": "YOUR_SECRET_KEY_HERE_DO_NOT_SHARE_THIS_AND_MAKE_IT_LONG_ENOUGH", + "Issuer": "OptixServe", + "Audience": "OptixServeUsers", + "TokenExpirationMinutes": 60 } } -} +} \ No newline at end of file diff --git a/OptixServe.Core/Services/UserService.cs b/OptixServe.Core/Services/UserService.cs index 01c4d7d..394f05c 100644 --- a/OptixServe.Core/Services/UserService.cs +++ b/OptixServe.Core/Services/UserService.cs @@ -7,6 +7,7 @@ public interface IUserService { IEnumerable GetUsers(); User? GetUserById(string id); + User? GetUserByUsername(string username); } public class UserService(AppDbContext dbContext) : IUserService @@ -18,6 +19,11 @@ public class UserService(AppDbContext dbContext) : IUserService return _dbContext.Users.FirstOrDefault(u => u.Id == id); } + public User? GetUserByUsername(string username) + { + return _dbContext.Users.FirstOrDefault(u => u.UserName == username); + } + public IEnumerable GetUsers() { return _dbContext.Users.AsEnumerable(); From 47cbdc21c1bf95d78f86760bf6ec85dab0ee4774 Mon Sep 17 00:00:00 2001 From: Huxley Deng Date: Fri, 11 Jul 2025 14:26:40 +0800 Subject: [PATCH 6/6] Ignore data and data_dev folder in git. Fix: gitignore now ignore /data and /data_dev folders which are runtime app data. --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 986ec50..c50af21 100644 --- a/.gitignore +++ b/.gitignore @@ -482,3 +482,7 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp + +# App data files and data for dev +/data +/data_dev