Compare commits
	
		
			8 Commits
		
	
	
		
			1e4aaf33f9
			...
			nativeaot-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| eb021576a5 | |||
| 8b18de1735 | |||
| 47cbdc21c1 | |||
| 724b1d4dae | |||
| 7cce413f79 | |||
| 6fd6c9f20d | |||
| dd5d556963 | |||
| 39b28386ae | 
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -482,3 +482,7 @@ $RECYCLE.BIN/ | |||||||
|  |  | ||||||
| # Vim temporary swap files | # Vim temporary swap files | ||||||
| *.swp | *.swp | ||||||
|  |  | ||||||
|  | # App data files and data for dev | ||||||
|  | /data | ||||||
|  | /data_dev | ||||||
|  | |||||||
							
								
								
									
										21
									
								
								OptixServe.Api/Configuration/ApiConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								OptixServe.Api/Configuration/ApiConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | namespace OptixServe.Api.Configuration; | ||||||
|  |  | ||||||
|  | public record ApiConfiguration | ||||||
|  | { | ||||||
|  |     public ApiSettings? Api { get; set; } = new(); | ||||||
|  |     public JwtSettings? Jwt { get; set; } = new(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public record ApiSettings | ||||||
|  | { | ||||||
|  |     public string? Listen { get; set; } | ||||||
|  |     public int? Port { get; set; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								OptixServe.Api/Dtos/Auth.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								OptixServe.Api/Dtos/Auth.cs
									
									
									
									
									
										Normal file
									
								
							| @ -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; } | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								OptixServe.Api/Dtos/Error.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								OptixServe.Api/Dtos/Error.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | namespace OptixServe.Api.Dtos; | ||||||
|  |  | ||||||
|  | public record CommonErrorDto | ||||||
|  | { | ||||||
|  |     public string? Message { get; set; } | ||||||
|  | } | ||||||
| @ -1,32 +1,60 @@ | |||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
|  | using OptixServe.Application.Services; | ||||||
| using OptixServe.Api.Dtos; | using OptixServe.Api.Dtos; | ||||||
|  | using OptixServe.Api.Services; | ||||||
|  |  | ||||||
| namespace OptixServe.Api.Endpoints; | namespace OptixServe.Api.Endpoints; | ||||||
|  |  | ||||||
|  |  | ||||||
| [JsonSerializable(typeof(UserDto))] | [JsonSerializable(typeof(UserDto))] | ||||||
| [JsonSerializable(typeof(IEnumerable<UserDto>))] | [JsonSerializable(typeof(IEnumerable<UserDto>))] | ||||||
|  | [JsonSerializable(typeof(LoginRequestDto))] | ||||||
|  | [JsonSerializable(typeof(LoginResponseDto))] | ||||||
| public partial class UserJsonContext : JsonSerializerContext { } | public partial class UserJsonContext : JsonSerializerContext { } | ||||||
|  |  | ||||||
| public static class UserEndpoint | public static class UserEndpoint | ||||||
| { | { | ||||||
|     public static IEnumerable<UserDto> GetUsers() |     public static void Register(RouteGroupBuilder parentGroup) | ||||||
|     { |     { | ||||||
|         return [ |         var group = parentGroup.MapGroup("/users"); | ||||||
|             new() {Id="1234", UserName = "xxx"}, |  | ||||||
|             new() {Id="5678", UserName = "yyy"}, |         group.MapPost("/login", LoginUser); | ||||||
|         ]; |         group.MapGet("/", GetAllUsers).RequireAuthorization(); | ||||||
|  |         group.MapGet("/{id}", GetUserById).RequireAuthorization(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static void Register(WebApplication app) |     public static IResult LoginUser(LoginRequestDto loginRequest, IUserService userService, ITokenService tokenService) | ||||||
|     { |     { | ||||||
|         var group = app.MapGroup("/users"); |         if (string.IsNullOrEmpty(loginRequest.UserName) || string.IsNullOrEmpty(loginRequest.Password)) | ||||||
|  |         { | ||||||
|         group.MapGet("/", GetAllUsers); |             return Results.BadRequest("Username and password are required."); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     public static IResult GetAllUsers() |         // Password hashing and salting will be implemented later. | ||||||
|  |         var user = userService.GetUserByUsername(loginRequest.UserName); | ||||||
|  |  | ||||||
|  |         if (user == null || user.Password != loginRequest.Password) | ||||||
|         { |         { | ||||||
|         return Results.Ok(GetUsers()); |             return Results.Unauthorized(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var token = tokenService.GenerateToken(user); | ||||||
|  |         return Results.Ok(new LoginResponseDto { Token = token }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static IResult GetAllUsers(IUserService userService) | ||||||
|  |     { | ||||||
|  |         var users = userService.GetUsers() | ||||||
|  |             .Select(u => new UserDto { Id = u.Id, UserName = u.UserName }); | ||||||
|  |         return Results.Ok(users); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static IResult GetUserById(string id, IUserService userService) | ||||||
|  |     { | ||||||
|  |         var user = userService.GetUserById(id); | ||||||
|  |         if (user == null) | ||||||
|  |             return Results.NotFound(); | ||||||
|  |  | ||||||
|  |         return Results.Ok(new UserDto { Id = user.Id, UserName = user.UserName }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										35
									
								
								OptixServe.Api/Endpoints/VersionEndpoint.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								OptixServe.Api/Endpoints/VersionEndpoint.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | using System.Text.Json.Serialization; | ||||||
|  | using Microsoft.Extensions.Options; | ||||||
|  | using OptixServe.Infrastructure.Configuration; | ||||||
|  | using OptixServe.Api.Dtos; | ||||||
|  |  | ||||||
|  | namespace OptixServe.Api.Endpoints; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | [JsonSerializable(typeof(string))] | ||||||
|  | [JsonSerializable(typeof(CommonErrorDto))] | ||||||
|  | public partial class VersionJsonContext : JsonSerializerContext { } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | /// This is a endpoint ONLY FOR TEST! | ||||||
|  | /// Should not expect ANY stable behavior on it! | ||||||
|  | /// </summary> | ||||||
|  | public static class VersionEndpoint | ||||||
|  | { | ||||||
|  |     public static void Register(RouteGroupBuilder parentGroup) | ||||||
|  |     { | ||||||
|  |         var group = parentGroup.MapGroup("/version"); | ||||||
|  |  | ||||||
|  |         group.MapGet("/", () => "v1"); | ||||||
|  |         group.MapGet("/test/dbconfig", (IOptions<InfrastructureConfiguration> infrastructureConfig) => | ||||||
|  |         { | ||||||
|  |             var dbType = infrastructureConfig.Value.Database?.Type; | ||||||
|  |             var dbHost = infrastructureConfig.Value.Database?.Host; | ||||||
|  |             return Results.Ok(new CommonErrorDto | ||||||
|  |             { | ||||||
|  |                 Message = $"Set up {dbType} database on {dbHost}" | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -2,9 +2,12 @@ | |||||||
|  |  | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <ProjectReference Include="..\OptixServe.Core\OptixServe.Core.csproj" /> |     <ProjectReference Include="..\OptixServe.Core\OptixServe.Core.csproj" /> | ||||||
|  |     <ProjectReference Include="..\OptixServe.Application\OptixServe.Application.csproj" /> | ||||||
|  |     <ProjectReference Include="..\OptixServe.Infrastructure\OptixServe.Infrastructure.csproj" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|  |  | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|  |     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" /> | ||||||
|     <PackageReference Include="System.CommandLine" Version="2.0.0-beta5.25306.1" /> |     <PackageReference Include="System.CommandLine" Version="2.0.0-beta5.25306.1" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,5 +1,14 @@ | |||||||
| using System.CommandLine; | 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.Endpoints; | ||||||
|  | using OptixServe.Api.Services; | ||||||
|  | using OptixServe.Application.Services; | ||||||
|  | using OptixServe.Infrastructure.Configuration; | ||||||
|  | using OptixServe.Infrastructure.Data; | ||||||
|  | using OptixServe.Infrastructure.Utilites; | ||||||
|  |  | ||||||
| class Program | class Program | ||||||
| { | { | ||||||
| @ -41,10 +50,22 @@ class Program | |||||||
|             var configFile = parseResult.GetValue(configOption); |             var configFile = parseResult.GetValue(configOption); | ||||||
|             builder.AddConfigurationWithCommand(configFile); |             builder.AddConfigurationWithCommand(configFile); | ||||||
|  |  | ||||||
|  |             builder.RegisterServices(); | ||||||
|             builder.RegiserJsonContext(); |             builder.RegiserJsonContext(); | ||||||
|  |  | ||||||
|             var app = builder.Build(); |             var app = builder.Build(); | ||||||
|             app.RegisterEndpoints(); |  | ||||||
|  |             app.UseAuthentication(); | ||||||
|  |             app.UseAuthorization(); | ||||||
|  |  | ||||||
|  |             using (var scope = app.Services.CreateScope()) | ||||||
|  |             { | ||||||
|  |                 var initializer = scope.ServiceProvider.GetRequiredService<DbInitializer>(); | ||||||
|  |                 initializer.Initialize(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var apiGroup = app.MapGroup("api/v1"); | ||||||
|  |             StartupHelper.RegisterEndpoints(apiGroup); | ||||||
|  |  | ||||||
|             app.Run(); |             app.Run(); | ||||||
|         }); |         }); | ||||||
| @ -59,15 +80,79 @@ class Program | |||||||
| /// <summary> | /// <summary> | ||||||
| /// Contains extension methods for WebApplicationBuilder and WebApplication | /// Contains extension methods for WebApplicationBuilder and WebApplication | ||||||
| /// </summary> | /// </summary> | ||||||
| static class ExtensionMethods | static class StartupHelper | ||||||
| { | { | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Registers all API endpoints |     /// Adds configuration sources to the application builder | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     /// <param name="app">WebApplication instance</param> |     /// <param name="builder">WebApplicationBuilder instance</param> | ||||||
|     public static void RegisterEndpoints(this WebApplication app) |     /// <param name="file">Optional configuration file specified via command line</param> | ||||||
|  |     public static void AddConfigurationWithCommand(this WebApplicationBuilder builder, FileInfo? file) | ||||||
|     { |     { | ||||||
|         UserEndpoint.Register(app); |         // Configure configuration sources in specified order | ||||||
|  |         var configurationBuilder = ConfigurationHelper.CreateDefaultBuilder(); | ||||||
|  |  | ||||||
|  |         // Add command-line specified config file if provided | ||||||
|  |         if (file != null) | ||||||
|  |         { | ||||||
|  |             configurationBuilder.AddJsonFile(file.FullName, optional: false); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         builder.Configuration.AddConfiguration(configurationBuilder.Build()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Configures DbContext services | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="services"></param> | ||||||
|  |     /// <param name="configuration"></param> | ||||||
|  |     /// <returns></returns> | ||||||
|  |     public static IServiceCollection AddAppDatabase(this IServiceCollection services, DatabaseSettings dbSettings) | ||||||
|  |     { | ||||||
|  |         services.AddDbContext<AppDbContext>(options => DatabaseHelper.ConfigureDbContext(options, dbSettings)); | ||||||
|  |         return services; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Configures services for DI | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="builder">WebApplicationBuilder instance</param> | ||||||
|  |     public static void RegisterServices(this WebApplicationBuilder builder) | ||||||
|  |     { | ||||||
|  |         // Add configuration classes | ||||||
|  |         var apiConfigSection = builder.Configuration.GetSection("OptixServe:Api"); | ||||||
|  |         builder.Services.Configure<ApiConfiguration>(apiConfigSection); | ||||||
|  |         var apiConfig = apiConfigSection.Get<ApiConfiguration>(); | ||||||
|  |  | ||||||
|  |         var infraConfigSection = builder.Configuration.GetSection("OptixServe:Infrastructure"); | ||||||
|  |         builder.Services.Configure<InfrastructureConfiguration>(infraConfigSection); | ||||||
|  |         var infraConfig = infraConfigSection.Get<InfrastructureConfiguration>(); | ||||||
|  |  | ||||||
|  |         // Add DBContext class | ||||||
|  |         builder.Services.AddAppDatabase(infraConfig?.Database!); | ||||||
|  |         builder.Services.AddScoped<DbInitializer>(); | ||||||
|  |  | ||||||
|  |         // Application services | ||||||
|  |         builder.Services.AddScoped<IUserService, UserService>(); | ||||||
|  |         builder.Services.AddScoped<ITokenService, TokenService>(); | ||||||
|  |  | ||||||
|  |         // Add Authentication and Authorization | ||||||
|  |         builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) | ||||||
|  |             .AddJwtBearer(options => | ||||||
|  |             { | ||||||
|  |                 var jwtSettings = apiConfig?.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(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
| @ -79,30 +164,18 @@ static class ExtensionMethods | |||||||
|         builder.Services.ConfigureHttpJsonOptions(options => |         builder.Services.ConfigureHttpJsonOptions(options => | ||||||
|         { |         { | ||||||
|             options.SerializerOptions.TypeInfoResolverChain.Add(UserJsonContext.Default); |             options.SerializerOptions.TypeInfoResolverChain.Add(UserJsonContext.Default); | ||||||
|  |             options.SerializerOptions.TypeInfoResolverChain.Add(VersionJsonContext.Default); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Adds configuration sources to the application builder |     /// Registers all API endpoints | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     /// <param name="builder">WebApplicationBuilder instance</param> |     /// <param name="rootGroup">Root RouteGroupBuilder instance</param> | ||||||
|     /// <param name="file">Optional configuration file specified via command line</param> |     public static void RegisterEndpoints(RouteGroupBuilder rootGroup) | ||||||
|     public static void AddConfigurationWithCommand(this WebApplicationBuilder builder, FileInfo? file) |  | ||||||
|     { |     { | ||||||
|         // Configure configuration sources in specified order |         UserEndpoint.Register(rootGroup); | ||||||
|         var configurationBuilder = new ConfigurationBuilder() |         VersionEndpoint.Register(rootGroup); | ||||||
|             .SetBasePath(Directory.GetCurrentDirectory()) |  | ||||||
|             .AddJsonFile("appsettings.json", optional: true) |  | ||||||
|             .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true) |  | ||||||
|             .AddJsonFile("config.json", optional: true) |  | ||||||
|             .AddJsonFile($"config.{builder.Environment.EnvironmentName}.json", optional: true); |  | ||||||
|  |  | ||||||
|         // Add command-line specified config file if provided |  | ||||||
|         if (file != null) |  | ||||||
|         { |  | ||||||
|             configurationBuilder.AddJsonFile(file.FullName, optional: false); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|         builder.Configuration.AddConfiguration(configurationBuilder.Build()); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
							
								
								
									
										44
									
								
								OptixServe.Api/Services/TokenService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								OptixServe.Api/Services/TokenService.cs
									
									
									
									
									
										Normal file
									
								
							| @ -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<ApiConfiguration> apiConfiguration) : ITokenService | ||||||
|  | { | ||||||
|  |     private readonly JwtSettings _jwtSettings = apiConfiguration.Value.Jwt ?? throw new ArgumentNullException(nameof(apiConfiguration), "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<Claim> | ||||||
|  |         { | ||||||
|  |             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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -5,5 +5,23 @@ | |||||||
|       "Microsoft.AspNetCore": "Warning" |       "Microsoft.AspNetCore": "Warning" | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "AllowedHosts": "*" |   "AllowedHosts": "*", | ||||||
|  |   "OptixServe": { | ||||||
|  |     "Api": { | ||||||
|  |       "Listen": "0.0.0.0", | ||||||
|  |       "Port": "54321", | ||||||
|  |       "Jwt": { | ||||||
|  |         "Secret": "YOUR_SECRET_KEY_HERE_DO_NOT_SHARE_THIS_AND_MAKE_IT_LONG_ENOUGH", | ||||||
|  |         "Issuer": "OptixServe", | ||||||
|  |         "Audience": "OptixServeUsers", | ||||||
|  |         "TokenExpirationMinutes": 60 | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "Infrastructure": { | ||||||
|  |       "Database": { | ||||||
|  |         "Type": "Sqlite", | ||||||
|  |         "Host": "optixserve.db" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										18
									
								
								OptixServe.Application/OptixServe.Application.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								OptixServe.Application/OptixServe.Application.csproj
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|  |  | ||||||
|  |   <ItemGroup> | ||||||
|  |     <ProjectReference Include="..\OptixServe.Core\OptixServe.Core.csproj" /> | ||||||
|  |     <ProjectReference Include="..\OptixServe.Infrastructure\OptixServe.Infrastructure.csproj" /> | ||||||
|  |   </ItemGroup> | ||||||
|  |  | ||||||
|  |   <ItemGroup> | ||||||
|  |     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" /> | ||||||
|  |   </ItemGroup> | ||||||
|  |  | ||||||
|  |   <PropertyGroup> | ||||||
|  |     <TargetFramework>net9.0</TargetFramework> | ||||||
|  |     <ImplicitUsings>enable</ImplicitUsings> | ||||||
|  |     <Nullable>enable</Nullable> | ||||||
|  |   </PropertyGroup> | ||||||
|  |  | ||||||
|  | </Project> | ||||||
							
								
								
									
										31
									
								
								OptixServe.Application/Services/UserService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								OptixServe.Application/Services/UserService.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | |||||||
|  | using OptixServe.Core.Models; | ||||||
|  | using OptixServe.Infrastructure.Data; | ||||||
|  |  | ||||||
|  | namespace OptixServe.Application.Services; | ||||||
|  |  | ||||||
|  | public interface IUserService | ||||||
|  | { | ||||||
|  |     IEnumerable<User> GetUsers(); | ||||||
|  |     User? GetUserById(string id); | ||||||
|  |     User? GetUserByUsername(string username); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public class UserService(AppDbContext dbContext) : IUserService | ||||||
|  | { | ||||||
|  |     private readonly AppDbContext _dbContext = dbContext; | ||||||
|  |  | ||||||
|  |     public User? GetUserById(string id) | ||||||
|  |     { | ||||||
|  |         return _dbContext.Users.FirstOrDefault(u => u.Id == id); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public User? GetUserByUsername(string username) | ||||||
|  |     { | ||||||
|  |         return _dbContext.Users.FirstOrDefault(u => u.UserName == username); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public IEnumerable<User> GetUsers() | ||||||
|  |     { | ||||||
|  |         return _dbContext.Users.AsEnumerable(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,8 +1,15 @@ | |||||||
| namespace OptixServe.Core.Models; | namespace OptixServe.Core.Models; | ||||||
|  |  | ||||||
|  | public enum PrivilegeGroup | ||||||
|  | { | ||||||
|  |     Admin, | ||||||
|  |     User, | ||||||
|  | } | ||||||
|  |  | ||||||
| public record User | public record User | ||||||
| { | { | ||||||
|     public required string Id { get; set; } |     public required string Id { get; set; } | ||||||
|     public required string UserName { 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; | ||||||
| } | } | ||||||
| @ -6,4 +6,5 @@ | |||||||
|     <Nullable>enable</Nullable> |     <Nullable>enable</Nullable> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|  |  | ||||||
|  |  | ||||||
| </Project> | </Project> | ||||||
|  | |||||||
| @ -0,0 +1,32 @@ | |||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
|  |  | ||||||
|  | namespace OptixServe.Infrastructure.Configuration; | ||||||
|  |  | ||||||
|  | public static class ConfigurationHelper | ||||||
|  | { | ||||||
|  |     public static IConfigurationBuilder CreateDefaultBuilder(string basePath) | ||||||
|  |     { | ||||||
|  |         var aspEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); | ||||||
|  |         var netEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); | ||||||
|  |         var env = aspEnv ?? netEnv ?? null; | ||||||
|  |  | ||||||
|  |         var builder = new ConfigurationBuilder() | ||||||
|  |             // .SetBasePath(Directory.GetCurrentDirectory()) | ||||||
|  |             .SetBasePath(basePath) | ||||||
|  |             .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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static IConfigurationBuilder CreateDefaultBuilder() | ||||||
|  |     { | ||||||
|  |         return CreateDefaultBuilder(Directory.GetCurrentDirectory()); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,18 @@ | |||||||
|  | namespace OptixServe.Infrastructure.Configuration; | ||||||
|  |  | ||||||
|  | public record InfrastructureConfiguration | ||||||
|  | { | ||||||
|  |     public DatabaseSettings? Database { get; set; } = new(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public enum DatabaseType | ||||||
|  | { | ||||||
|  |     Sqlite, | ||||||
|  |     MySQL | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public record DatabaseSettings | ||||||
|  | { | ||||||
|  |     public DatabaseType Type { get; set; } = DatabaseType.Sqlite; | ||||||
|  |     public string? Host { get; set; } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								OptixServe.Infrastructure/Data/AppDbContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								OptixServe.Infrastructure/Data/AppDbContext.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using OptixServe.Core.Models; | ||||||
|  |  | ||||||
|  | namespace OptixServe.Infrastructure.Data; | ||||||
|  |  | ||||||
|  | public class AppDbContext(DbContextOptions options) : DbContext(options) | ||||||
|  | { | ||||||
|  |     public DbSet<User> Users { get; set; } = null!; | ||||||
|  |  | ||||||
|  |     protected override void OnModelCreating(ModelBuilder modelBuilder) | ||||||
|  |     { | ||||||
|  |         modelBuilder.Entity<User>(user => | ||||||
|  |         { | ||||||
|  |             user.HasKey(u => u.Id); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         modelBuilder.Entity<User>().HasData([ | ||||||
|  |             new() {Id = "1", UserName = "admin", Password = "admin12345"} | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,9 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using OptixServe.Infrastructure.Data; | ||||||
|  | using OptixServe.Infrastructure.Data.CompiledModels; | ||||||
|  |  | ||||||
|  | #pragma warning disable 219, 612, 618 | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | [assembly: DbContextModel(typeof(AppDbContext), typeof(AppDbContextModel))] | ||||||
| @ -0,0 +1,47 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Metadata; | ||||||
|  |  | ||||||
|  | #pragma warning disable 219, 612, 618 | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace OptixServe.Infrastructure.Data.CompiledModels | ||||||
|  | { | ||||||
|  |     [DbContext(typeof(AppDbContext))] | ||||||
|  |     public partial class AppDbContextModel : RuntimeModel | ||||||
|  |     { | ||||||
|  |         private static readonly bool _useOldBehavior31751 = | ||||||
|  |             System.AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue31751", out var enabled31751) && enabled31751; | ||||||
|  |  | ||||||
|  |         static AppDbContextModel() | ||||||
|  |         { | ||||||
|  |             var model = new AppDbContextModel(); | ||||||
|  |  | ||||||
|  |             if (_useOldBehavior31751) | ||||||
|  |             { | ||||||
|  |                 model.Initialize(); | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 var thread = new System.Threading.Thread(RunInitialization, 10 * 1024 * 1024); | ||||||
|  |                 thread.Start(); | ||||||
|  |                 thread.Join(); | ||||||
|  |  | ||||||
|  |                 void RunInitialization() | ||||||
|  |                 { | ||||||
|  |                     model.Initialize(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             model.Customize(); | ||||||
|  |             _instance = (AppDbContextModel)model.FinalizeModel(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private static AppDbContextModel _instance; | ||||||
|  |         public static IModel Instance => _instance; | ||||||
|  |  | ||||||
|  |         partial void Initialize(); | ||||||
|  |  | ||||||
|  |         partial void Customize(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,99 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Metadata; | ||||||
|  | using Microsoft.EntityFrameworkCore.Metadata.Internal; | ||||||
|  | using Microsoft.EntityFrameworkCore.Update.Internal; | ||||||
|  |  | ||||||
|  | #pragma warning disable 219, 612, 618 | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace OptixServe.Infrastructure.Data.CompiledModels | ||||||
|  | { | ||||||
|  |     public partial class AppDbContextModel | ||||||
|  |     { | ||||||
|  |         private AppDbContextModel() | ||||||
|  |             : base(skipDetectChanges: false, modelId: new Guid("3214a553-d69f-4844-a587-080f6d463671"), entityTypeCount: 1) | ||||||
|  |         { | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         partial void Initialize() | ||||||
|  |         { | ||||||
|  |             var user = UserEntityType.Create(this); | ||||||
|  |  | ||||||
|  |             UserEntityType.CreateAnnotations(user); | ||||||
|  |  | ||||||
|  |             AddAnnotation("ProductVersion", "9.0.6"); | ||||||
|  |             AddRuntimeAnnotation("Relational:RelationalModelFactory", () => CreateRelationalModel()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private IRelationalModel CreateRelationalModel() | ||||||
|  |         { | ||||||
|  |             var relationalModel = new RelationalModel(this); | ||||||
|  |  | ||||||
|  |             var user = FindEntityType("OptixServe.Core.Models.User")!; | ||||||
|  |  | ||||||
|  |             var defaultTableMappings = new List<TableMappingBase<ColumnMappingBase>>(); | ||||||
|  |             user.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings); | ||||||
|  |             var optixServeCoreModelsUserTableBase = new TableBase("OptixServe.Core.Models.User", null, relationalModel); | ||||||
|  |             var idColumnBase = new ColumnBase<ColumnMappingBase>("Id", "TEXT", optixServeCoreModelsUserTableBase); | ||||||
|  |             optixServeCoreModelsUserTableBase.Columns.Add("Id", idColumnBase); | ||||||
|  |             var passwordColumnBase = new ColumnBase<ColumnMappingBase>("Password", "TEXT", optixServeCoreModelsUserTableBase) | ||||||
|  |             { | ||||||
|  |                 IsNullable = true | ||||||
|  |             }; | ||||||
|  |             optixServeCoreModelsUserTableBase.Columns.Add("Password", passwordColumnBase); | ||||||
|  |             var privilegeGroupColumnBase = new ColumnBase<ColumnMappingBase>("PrivilegeGroup", "INTEGER", optixServeCoreModelsUserTableBase); | ||||||
|  |             optixServeCoreModelsUserTableBase.Columns.Add("PrivilegeGroup", privilegeGroupColumnBase); | ||||||
|  |             var userNameColumnBase = new ColumnBase<ColumnMappingBase>("UserName", "TEXT", optixServeCoreModelsUserTableBase); | ||||||
|  |             optixServeCoreModelsUserTableBase.Columns.Add("UserName", userNameColumnBase); | ||||||
|  |             relationalModel.DefaultTables.Add("OptixServe.Core.Models.User", optixServeCoreModelsUserTableBase); | ||||||
|  |             var optixServeCoreModelsUserMappingBase = new TableMappingBase<ColumnMappingBase>(user, optixServeCoreModelsUserTableBase, null); | ||||||
|  |             optixServeCoreModelsUserTableBase.AddTypeMapping(optixServeCoreModelsUserMappingBase, false); | ||||||
|  |             defaultTableMappings.Add(optixServeCoreModelsUserMappingBase); | ||||||
|  |             RelationalModel.CreateColumnMapping((ColumnBase<ColumnMappingBase>)idColumnBase, user.FindProperty("Id")!, optixServeCoreModelsUserMappingBase); | ||||||
|  |             RelationalModel.CreateColumnMapping((ColumnBase<ColumnMappingBase>)passwordColumnBase, user.FindProperty("Password")!, optixServeCoreModelsUserMappingBase); | ||||||
|  |             RelationalModel.CreateColumnMapping((ColumnBase<ColumnMappingBase>)privilegeGroupColumnBase, user.FindProperty("PrivilegeGroup")!, optixServeCoreModelsUserMappingBase); | ||||||
|  |             RelationalModel.CreateColumnMapping((ColumnBase<ColumnMappingBase>)userNameColumnBase, user.FindProperty("UserName")!, optixServeCoreModelsUserMappingBase); | ||||||
|  |  | ||||||
|  |             var tableMappings = new List<TableMapping>(); | ||||||
|  |             user.SetRuntimeAnnotation("Relational:TableMappings", tableMappings); | ||||||
|  |             var usersTable = new Table("Users", null, relationalModel); | ||||||
|  |             var idColumn = new Column("Id", "TEXT", usersTable); | ||||||
|  |             usersTable.Columns.Add("Id", idColumn); | ||||||
|  |             idColumn.Accessors = ColumnAccessorsFactory.CreateGeneric<string>(idColumn); | ||||||
|  |             var passwordColumn = new Column("Password", "TEXT", usersTable) | ||||||
|  |             { | ||||||
|  |                 IsNullable = true | ||||||
|  |             }; | ||||||
|  |             usersTable.Columns.Add("Password", passwordColumn); | ||||||
|  |             passwordColumn.Accessors = ColumnAccessorsFactory.CreateGeneric<string>(passwordColumn); | ||||||
|  |             var privilegeGroupColumn = new Column("PrivilegeGroup", "INTEGER", usersTable); | ||||||
|  |             usersTable.Columns.Add("PrivilegeGroup", privilegeGroupColumn); | ||||||
|  |             privilegeGroupColumn.Accessors = ColumnAccessorsFactory.CreateGeneric<int>(privilegeGroupColumn); | ||||||
|  |             var userNameColumn = new Column("UserName", "TEXT", usersTable); | ||||||
|  |             usersTable.Columns.Add("UserName", userNameColumn); | ||||||
|  |             userNameColumn.Accessors = ColumnAccessorsFactory.CreateGeneric<string>(userNameColumn); | ||||||
|  |             relationalModel.Tables.Add(("Users", null), usersTable); | ||||||
|  |             var usersTableMapping = new TableMapping(user, usersTable, null); | ||||||
|  |             usersTable.AddTypeMapping(usersTableMapping, false); | ||||||
|  |             tableMappings.Add(usersTableMapping); | ||||||
|  |             RelationalModel.CreateColumnMapping(idColumn, user.FindProperty("Id")!, usersTableMapping); | ||||||
|  |             RelationalModel.CreateColumnMapping(passwordColumn, user.FindProperty("Password")!, usersTableMapping); | ||||||
|  |             RelationalModel.CreateColumnMapping(privilegeGroupColumn, user.FindProperty("PrivilegeGroup")!, usersTableMapping); | ||||||
|  |             RelationalModel.CreateColumnMapping(userNameColumn, user.FindProperty("UserName")!, usersTableMapping); | ||||||
|  |             var pK_Users = new UniqueConstraint("PK_Users", usersTable, new[] { idColumn }); | ||||||
|  |             usersTable.PrimaryKey = pK_Users; | ||||||
|  |             pK_Users.SetRowKeyValueFactory(new SimpleRowKeyValueFactory<string>(pK_Users)); | ||||||
|  |             var pK_UsersKey = RelationalModel.GetKey(this, | ||||||
|  |                 "OptixServe.Core.Models.User", | ||||||
|  |                 new[] { "Id" }); | ||||||
|  |             pK_Users.MappedKeys.Add(pK_UsersKey); | ||||||
|  |             RelationalModel.GetOrCreateUniqueConstraints(pK_UsersKey).Add(pK_Users); | ||||||
|  |             usersTable.UniqueConstraints.Add("PK_Users", pK_Users); | ||||||
|  |             return relationalModel.MakeReadOnly(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										227
									
								
								OptixServe.Infrastructure/Data/CompiledModels/UserEntityType.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								OptixServe.Infrastructure/Data/CompiledModels/UserEntityType.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,227 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Reflection; | ||||||
|  | using Microsoft.EntityFrameworkCore.ChangeTracking; | ||||||
|  | using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Metadata; | ||||||
|  | using Microsoft.EntityFrameworkCore.Metadata.Internal; | ||||||
|  | using Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage.Json; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
|  | using OptixServe.Core.Models; | ||||||
|  |  | ||||||
|  | #pragma warning disable 219, 612, 618 | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace OptixServe.Infrastructure.Data.CompiledModels | ||||||
|  | { | ||||||
|  |     [EntityFrameworkInternal] | ||||||
|  |     public partial class UserEntityType | ||||||
|  |     { | ||||||
|  |         public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null) | ||||||
|  |         { | ||||||
|  |             var runtimeEntityType = model.AddEntityType( | ||||||
|  |                 "OptixServe.Core.Models.User", | ||||||
|  |                 typeof(User), | ||||||
|  |                 baseEntityType, | ||||||
|  |                 propertyCount: 4, | ||||||
|  |                 keyCount: 1); | ||||||
|  |  | ||||||
|  |             var id = runtimeEntityType.AddProperty( | ||||||
|  |                 "Id", | ||||||
|  |                 typeof(string), | ||||||
|  |                 propertyInfo: typeof(User).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), | ||||||
|  |                 fieldInfo: typeof(User).GetField("<Id>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), | ||||||
|  |                 afterSaveBehavior: PropertySaveBehavior.Throw); | ||||||
|  |             id.SetGetter( | ||||||
|  |                 string (User entity) => UserUnsafeAccessors.Id(entity), | ||||||
|  |                 bool (User entity) => UserUnsafeAccessors.Id(entity) == null, | ||||||
|  |                 string (User instance) => UserUnsafeAccessors.Id(instance), | ||||||
|  |                 bool (User instance) => UserUnsafeAccessors.Id(instance) == null); | ||||||
|  |             id.SetSetter( | ||||||
|  |                 (User entity, string value) => UserUnsafeAccessors.Id(entity) = value); | ||||||
|  |             id.SetMaterializationSetter( | ||||||
|  |                 (User entity, string value) => UserUnsafeAccessors.Id(entity) = value); | ||||||
|  |             id.SetAccessors( | ||||||
|  |                 string (InternalEntityEntry entry) => UserUnsafeAccessors.Id(((User)(entry.Entity))), | ||||||
|  |                 string (InternalEntityEntry entry) => UserUnsafeAccessors.Id(((User)(entry.Entity))), | ||||||
|  |                 string (InternalEntityEntry entry) => entry.ReadOriginalValue<string>(id, 0), | ||||||
|  |                 string (InternalEntityEntry entry) => entry.ReadRelationshipSnapshotValue<string>(id, 0), | ||||||
|  |                 object (ValueBuffer valueBuffer) => valueBuffer[0]); | ||||||
|  |             id.SetPropertyIndexes( | ||||||
|  |                 index: 0, | ||||||
|  |                 originalValueIndex: 0, | ||||||
|  |                 shadowIndex: -1, | ||||||
|  |                 relationshipIndex: 0, | ||||||
|  |                 storeGenerationIndex: -1); | ||||||
|  |             id.TypeMapping = SqliteStringTypeMapping.Default; | ||||||
|  |             id.SetCurrentValueComparer(new EntryCurrentValueComparer<string>(id)); | ||||||
|  |  | ||||||
|  |             var password = runtimeEntityType.AddProperty( | ||||||
|  |                 "Password", | ||||||
|  |                 typeof(string), | ||||||
|  |                 propertyInfo: typeof(User).GetProperty("Password", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), | ||||||
|  |                 fieldInfo: typeof(User).GetField("<Password>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), | ||||||
|  |                 nullable: true); | ||||||
|  |             password.SetGetter( | ||||||
|  |                 string (User entity) => UserUnsafeAccessors.Password(entity), | ||||||
|  |                 bool (User entity) => UserUnsafeAccessors.Password(entity) == null, | ||||||
|  |                 string (User instance) => UserUnsafeAccessors.Password(instance), | ||||||
|  |                 bool (User instance) => UserUnsafeAccessors.Password(instance) == null); | ||||||
|  |             password.SetSetter( | ||||||
|  |                 (User entity, string value) => UserUnsafeAccessors.Password(entity) = value); | ||||||
|  |             password.SetMaterializationSetter( | ||||||
|  |                 (User entity, string value) => UserUnsafeAccessors.Password(entity) = value); | ||||||
|  |             password.SetAccessors( | ||||||
|  |                 string (InternalEntityEntry entry) => UserUnsafeAccessors.Password(((User)(entry.Entity))), | ||||||
|  |                 string (InternalEntityEntry entry) => UserUnsafeAccessors.Password(((User)(entry.Entity))), | ||||||
|  |                 string (InternalEntityEntry entry) => entry.ReadOriginalValue<string>(password, 1), | ||||||
|  |                 string (InternalEntityEntry entry) => entry.GetCurrentValue<string>(password), | ||||||
|  |                 object (ValueBuffer valueBuffer) => valueBuffer[1]); | ||||||
|  |             password.SetPropertyIndexes( | ||||||
|  |                 index: 1, | ||||||
|  |                 originalValueIndex: 1, | ||||||
|  |                 shadowIndex: -1, | ||||||
|  |                 relationshipIndex: -1, | ||||||
|  |                 storeGenerationIndex: -1); | ||||||
|  |             password.TypeMapping = SqliteStringTypeMapping.Default; | ||||||
|  |  | ||||||
|  |             var privilegeGroup = runtimeEntityType.AddProperty( | ||||||
|  |                 "PrivilegeGroup", | ||||||
|  |                 typeof(PrivilegeGroup), | ||||||
|  |                 propertyInfo: typeof(User).GetProperty("PrivilegeGroup", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), | ||||||
|  |                 fieldInfo: typeof(User).GetField("<PrivilegeGroup>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); | ||||||
|  |             privilegeGroup.SetGetter( | ||||||
|  |                 PrivilegeGroup (User entity) => UserUnsafeAccessors.PrivilegeGroup(entity), | ||||||
|  |                 bool (User entity) => object.Equals(((object)(UserUnsafeAccessors.PrivilegeGroup(entity))), ((object)(PrivilegeGroup.Admin))), | ||||||
|  |                 PrivilegeGroup (User instance) => UserUnsafeAccessors.PrivilegeGroup(instance), | ||||||
|  |                 bool (User instance) => object.Equals(((object)(UserUnsafeAccessors.PrivilegeGroup(instance))), ((object)(PrivilegeGroup.Admin)))); | ||||||
|  |             privilegeGroup.SetSetter( | ||||||
|  |                 (User entity, PrivilegeGroup value) => UserUnsafeAccessors.PrivilegeGroup(entity) = value); | ||||||
|  |             privilegeGroup.SetMaterializationSetter( | ||||||
|  |                 (User entity, PrivilegeGroup value) => UserUnsafeAccessors.PrivilegeGroup(entity) = value); | ||||||
|  |             privilegeGroup.SetAccessors( | ||||||
|  |                 PrivilegeGroup (InternalEntityEntry entry) => UserUnsafeAccessors.PrivilegeGroup(((User)(entry.Entity))), | ||||||
|  |                 PrivilegeGroup (InternalEntityEntry entry) => UserUnsafeAccessors.PrivilegeGroup(((User)(entry.Entity))), | ||||||
|  |                 PrivilegeGroup (InternalEntityEntry entry) => entry.ReadOriginalValue<PrivilegeGroup>(privilegeGroup, 2), | ||||||
|  |                 PrivilegeGroup (InternalEntityEntry entry) => entry.GetCurrentValue<PrivilegeGroup>(privilegeGroup), | ||||||
|  |                 object (ValueBuffer valueBuffer) => valueBuffer[2]); | ||||||
|  |             privilegeGroup.SetPropertyIndexes( | ||||||
|  |                 index: 2, | ||||||
|  |                 originalValueIndex: 2, | ||||||
|  |                 shadowIndex: -1, | ||||||
|  |                 relationshipIndex: -1, | ||||||
|  |                 storeGenerationIndex: -1); | ||||||
|  |             privilegeGroup.TypeMapping = IntTypeMapping.Default.Clone( | ||||||
|  |                 comparer: new ValueComparer<PrivilegeGroup>( | ||||||
|  |                     bool (PrivilegeGroup v1, PrivilegeGroup v2) => object.Equals(((object)(v1)), ((object)(v2))), | ||||||
|  |                     int (PrivilegeGroup v) => ((object)v).GetHashCode(), | ||||||
|  |                     PrivilegeGroup (PrivilegeGroup v) => v), | ||||||
|  |                 keyComparer: new ValueComparer<PrivilegeGroup>( | ||||||
|  |                     bool (PrivilegeGroup v1, PrivilegeGroup v2) => object.Equals(((object)(v1)), ((object)(v2))), | ||||||
|  |                     int (PrivilegeGroup v) => ((object)v).GetHashCode(), | ||||||
|  |                     PrivilegeGroup (PrivilegeGroup v) => v), | ||||||
|  |                 providerValueComparer: new ValueComparer<int>( | ||||||
|  |                     bool (int v1, int v2) => v1 == v2, | ||||||
|  |                     int (int v) => v, | ||||||
|  |                     int (int v) => v), | ||||||
|  |                 mappingInfo: new RelationalTypeMappingInfo( | ||||||
|  |                     storeTypeName: "INTEGER"), | ||||||
|  |                 converter: new ValueConverter<PrivilegeGroup, int>( | ||||||
|  |                     int (PrivilegeGroup value) => ((int)(value)), | ||||||
|  |                     PrivilegeGroup (int value) => ((PrivilegeGroup)(value))), | ||||||
|  |                 jsonValueReaderWriter: new JsonConvertedValueReaderWriter<PrivilegeGroup, int>( | ||||||
|  |                     JsonInt32ReaderWriter.Instance, | ||||||
|  |                     new ValueConverter<PrivilegeGroup, int>( | ||||||
|  |                         int (PrivilegeGroup value) => ((int)(value)), | ||||||
|  |                         PrivilegeGroup (int value) => ((PrivilegeGroup)(value))))); | ||||||
|  |             privilegeGroup.SetSentinelFromProviderValue(0); | ||||||
|  |  | ||||||
|  |             var userName = runtimeEntityType.AddProperty( | ||||||
|  |                 "UserName", | ||||||
|  |                 typeof(string), | ||||||
|  |                 propertyInfo: typeof(User).GetProperty("UserName", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), | ||||||
|  |                 fieldInfo: typeof(User).GetField("<UserName>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); | ||||||
|  |             userName.SetGetter( | ||||||
|  |                 string (User entity) => UserUnsafeAccessors.UserName(entity), | ||||||
|  |                 bool (User entity) => UserUnsafeAccessors.UserName(entity) == null, | ||||||
|  |                 string (User instance) => UserUnsafeAccessors.UserName(instance), | ||||||
|  |                 bool (User instance) => UserUnsafeAccessors.UserName(instance) == null); | ||||||
|  |             userName.SetSetter( | ||||||
|  |                 (User entity, string value) => UserUnsafeAccessors.UserName(entity) = value); | ||||||
|  |             userName.SetMaterializationSetter( | ||||||
|  |                 (User entity, string value) => UserUnsafeAccessors.UserName(entity) = value); | ||||||
|  |             userName.SetAccessors( | ||||||
|  |                 string (InternalEntityEntry entry) => UserUnsafeAccessors.UserName(((User)(entry.Entity))), | ||||||
|  |                 string (InternalEntityEntry entry) => UserUnsafeAccessors.UserName(((User)(entry.Entity))), | ||||||
|  |                 string (InternalEntityEntry entry) => entry.ReadOriginalValue<string>(userName, 3), | ||||||
|  |                 string (InternalEntityEntry entry) => entry.GetCurrentValue<string>(userName), | ||||||
|  |                 object (ValueBuffer valueBuffer) => valueBuffer[3]); | ||||||
|  |             userName.SetPropertyIndexes( | ||||||
|  |                 index: 3, | ||||||
|  |                 originalValueIndex: 3, | ||||||
|  |                 shadowIndex: -1, | ||||||
|  |                 relationshipIndex: -1, | ||||||
|  |                 storeGenerationIndex: -1); | ||||||
|  |             userName.TypeMapping = SqliteStringTypeMapping.Default; | ||||||
|  |  | ||||||
|  |             var key = runtimeEntityType.AddKey( | ||||||
|  |                 new[] { id }); | ||||||
|  |             runtimeEntityType.SetPrimaryKey(key); | ||||||
|  |  | ||||||
|  |             return runtimeEntityType; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) | ||||||
|  |         { | ||||||
|  |             var id = runtimeEntityType.FindProperty("Id"); | ||||||
|  |             var password = runtimeEntityType.FindProperty("Password"); | ||||||
|  |             var privilegeGroup = runtimeEntityType.FindProperty("PrivilegeGroup"); | ||||||
|  |             var userName = runtimeEntityType.FindProperty("UserName"); | ||||||
|  |             var key = runtimeEntityType.FindKey(new[] { id }); | ||||||
|  |             key.SetPrincipalKeyValueFactory(KeyValueFactoryFactory.CreateSimpleNullableFactory<string, int>(key)); | ||||||
|  |             key.SetIdentityMapFactory(IdentityMapFactoryFactory.CreateFactory<string>(key)); | ||||||
|  |             runtimeEntityType.SetOriginalValuesFactory( | ||||||
|  |                 ISnapshot (InternalEntityEntry source) => | ||||||
|  |                 { | ||||||
|  |                     var entity = ((User)(source.Entity)); | ||||||
|  |                     return ((ISnapshot)(new Snapshot<string, string, PrivilegeGroup, string>((source.GetCurrentValue<string>(id) == null ? null : ((ValueComparer<string>)(((IProperty)id).GetValueComparer())).Snapshot(source.GetCurrentValue<string>(id))), (source.GetCurrentValue<string>(password) == null ? null : ((ValueComparer<string>)(((IProperty)password).GetValueComparer())).Snapshot(source.GetCurrentValue<string>(password))), ((ValueComparer<PrivilegeGroup>)(((IProperty)privilegeGroup).GetValueComparer())).Snapshot(source.GetCurrentValue<PrivilegeGroup>(privilegeGroup)), (source.GetCurrentValue<string>(userName) == null ? null : ((ValueComparer<string>)(((IProperty)userName).GetValueComparer())).Snapshot(source.GetCurrentValue<string>(userName)))))); | ||||||
|  |                 }); | ||||||
|  |             runtimeEntityType.SetStoreGeneratedValuesFactory( | ||||||
|  |                 ISnapshot () => Snapshot.Empty); | ||||||
|  |             runtimeEntityType.SetTemporaryValuesFactory( | ||||||
|  |                 ISnapshot (InternalEntityEntry source) => Snapshot.Empty); | ||||||
|  |             runtimeEntityType.SetShadowValuesFactory( | ||||||
|  |                 ISnapshot (IDictionary<string, object> source) => Snapshot.Empty); | ||||||
|  |             runtimeEntityType.SetEmptyShadowValuesFactory( | ||||||
|  |                 ISnapshot () => Snapshot.Empty); | ||||||
|  |             runtimeEntityType.SetRelationshipSnapshotFactory( | ||||||
|  |                 ISnapshot (InternalEntityEntry source) => | ||||||
|  |                 { | ||||||
|  |                     var entity = ((User)(source.Entity)); | ||||||
|  |                     return ((ISnapshot)(new Snapshot<string>((source.GetCurrentValue<string>(id) == null ? null : ((ValueComparer<string>)(((IProperty)id).GetKeyValueComparer())).Snapshot(source.GetCurrentValue<string>(id)))))); | ||||||
|  |                 }); | ||||||
|  |             runtimeEntityType.Counts = new PropertyCounts( | ||||||
|  |                 propertyCount: 4, | ||||||
|  |                 navigationCount: 0, | ||||||
|  |                 complexPropertyCount: 0, | ||||||
|  |                 originalValueCount: 4, | ||||||
|  |                 shadowCount: 0, | ||||||
|  |                 relationshipCount: 1, | ||||||
|  |                 storeGeneratedCount: 0); | ||||||
|  |             runtimeEntityType.AddAnnotation("Relational:FunctionName", null); | ||||||
|  |             runtimeEntityType.AddAnnotation("Relational:Schema", null); | ||||||
|  |             runtimeEntityType.AddAnnotation("Relational:SqlQuery", null); | ||||||
|  |             runtimeEntityType.AddAnnotation("Relational:TableName", "Users"); | ||||||
|  |             runtimeEntityType.AddAnnotation("Relational:ViewName", null); | ||||||
|  |             runtimeEntityType.AddAnnotation("Relational:ViewSchema", null); | ||||||
|  |  | ||||||
|  |             Customize(runtimeEntityType); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         static partial void Customize(RuntimeEntityType runtimeEntityType); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,25 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | using System.Runtime.CompilerServices; | ||||||
|  | using OptixServe.Core.Models; | ||||||
|  |  | ||||||
|  | #pragma warning disable 219, 612, 618 | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace OptixServe.Infrastructure.Data.CompiledModels | ||||||
|  | { | ||||||
|  |     public static class UserUnsafeAccessors | ||||||
|  |     { | ||||||
|  |         [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<Id>k__BackingField")] | ||||||
|  |         public static extern ref string Id(User @this); | ||||||
|  |  | ||||||
|  |         [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<Password>k__BackingField")] | ||||||
|  |         public static extern ref string Password(User @this); | ||||||
|  |  | ||||||
|  |         [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<PrivilegeGroup>k__BackingField")] | ||||||
|  |         public static extern ref PrivilegeGroup PrivilegeGroup(User @this); | ||||||
|  |  | ||||||
|  |         [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<UserName>k__BackingField")] | ||||||
|  |         public static extern ref string UserName(User @this); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								OptixServe.Infrastructure/Data/DbInitializer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								OptixServe.Infrastructure/Data/DbInitializer.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | namespace OptixServe.Infrastructure.Data; | ||||||
|  |  | ||||||
|  | public class DbInitializer(AppDbContext dbContext) | ||||||
|  | { | ||||||
|  |     private readonly AppDbContext _context = dbContext; | ||||||
|  |  | ||||||
|  |     public void Initialize() | ||||||
|  |     { | ||||||
|  |         _context.Database.EnsureCreated(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										82
									
								
								OptixServe.Infrastructure/EFCore-NativeAOT-Intergration.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								OptixServe.Infrastructure/EFCore-NativeAOT-Intergration.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,82 @@ | |||||||
|  | # EFCore Intergration | ||||||
|  |  | ||||||
|  | ## Introduction | ||||||
|  |  | ||||||
|  | This app relies on EF Core to access the database. | ||||||
|  |  | ||||||
|  | However, it is NOT so smooth when work with NativeAOT. | ||||||
|  |  | ||||||
|  | To use EF Core, see below. | ||||||
|  |  | ||||||
|  | ## DesignTimeDbContextFactory | ||||||
|  |  | ||||||
|  | To work with NativeAOT, a code-defined way to create DbContext instance is a MUST in nearly ALL operations of EF Core, letting ef tool discover the DbContext. | ||||||
|  |  | ||||||
|  | In this project, `OptixServe.Infrastructure/Utilites/DesignTimeDbContextFactory.cs` is set up to do this. | ||||||
|  |  | ||||||
|  | This class implements a simple commandline arguments parser, enabling passing arguments along with `dotnet ef`. | ||||||
|  |  | ||||||
|  | Currently, there are two options, `--config/-c` and `--data-dir/-d` are supported. See documents there. | ||||||
|  |  | ||||||
|  | To pass arguments/options to `CreateDbContext` static method, pass the arguments following with `dotnet ef` command, with two dashes splitting. | ||||||
|  |  | ||||||
|  | For example: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | dotnet ef database update -- -c ../data/appsettings.Development.json -d ../data/ | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Workflow | ||||||
|  |  | ||||||
|  | To make the code work, following the steps: | ||||||
|  |  | ||||||
|  | 1. Implement other parts | ||||||
|  | 2. Compile the data models, with `--nativeaot` option. | ||||||
|  | 3. Add migration | ||||||
|  | 4. Update database | ||||||
|  | 5. Build project, and Run! | ||||||
|  |  | ||||||
|  | Here are the details. | ||||||
|  |  | ||||||
|  | ### Requirements | ||||||
|  |  | ||||||
|  | See [https://learn.microsoft.com/en-us/ef/core/performance/nativeaot-and-precompiled-queries]() | ||||||
|  |  | ||||||
|  | ### Compile Models | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | dotnet ef dbcontext optimize --output-dir Data/CompiledModels --precompile-queries --nativeaot -- -c ../data_dev/appsettings.Development.json -d ../data_dev/ | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | If this is the first time to run `dbcontext optimize`, DO NOT forget to `UseModel` in DbContext construction. | ||||||
|  |  | ||||||
|  | ```cs OptixServe.Infrastructure/Utilites/DatabaseHelper.cs | ||||||
|  | public static void ConfigureDbContext(DbContextOptionsBuilder options) | ||||||
|  | { | ||||||
|  |     // ... | ||||||
|  |     options.UseSqlite(connectionString, b => b.MigrationsAssembly("OptixServe.Infrastructure")) | ||||||
|  |         .UseModel(AppDbContextModel.Instance); | ||||||
|  |     // ... | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Add Migration | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | dotnet ef migrations add InitialCreate -- -c ../data_dev/appsettings.Development.json | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Update Database | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | dotnet ef database update -- -c ../data_dev/appsettings.Development.json -d ../data_dev/ | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Build Project | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | dotnet publish -c Release -r linux-x64 | ||||||
|  | ``` | ||||||
							
								
								
									
										2
									
								
								OptixServe.Infrastructure/Migrations/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								OptixServe.Infrastructure/Migrations/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | # Migrations are ignored in development | ||||||
|  | *.cs | ||||||
							
								
								
									
										30
									
								
								OptixServe.Infrastructure/OptixServe.Infrastructure.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								OptixServe.Infrastructure/OptixServe.Infrastructure.csproj
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|  |  | ||||||
|  |   <ItemGroup> | ||||||
|  |     <ProjectReference Include="..\OptixServe.Core\OptixServe.Core.csproj" /> | ||||||
|  |   </ItemGroup> | ||||||
|  |  | ||||||
|  |   <ItemGroup> | ||||||
|  |     <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6"> | ||||||
|  |       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|  |       <PrivateAssets>all</PrivateAssets> | ||||||
|  |     </PackageReference> | ||||||
|  |     <PackageReference Include="Microsoft.EntityFrameworkCore.Tasks" Version="9.0.6"> | ||||||
|  |       <PrivateAssets>all</PrivateAssets> | ||||||
|  |       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|  |     </PackageReference> | ||||||
|  |     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" /> | ||||||
|  |     <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" /> | ||||||
|  |     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.6" /> | ||||||
|  |     <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.6" /> | ||||||
|  |     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.6" /> | ||||||
|  |   </ItemGroup> | ||||||
|  |  | ||||||
|  |   <PropertyGroup> | ||||||
|  |     <TargetFramework>net9.0</TargetFramework> | ||||||
|  |     <ImplicitUsings>enable</ImplicitUsings> | ||||||
|  |     <Nullable>enable</Nullable> | ||||||
|  |     <InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.EntityFrameworkCore.GeneratedInterceptors</InterceptorsNamespaces> | ||||||
|  |   </PropertyGroup> | ||||||
|  |  | ||||||
|  | </Project> | ||||||
							
								
								
									
										35
									
								
								OptixServe.Infrastructure/Utilites/DatabaseHelper.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								OptixServe.Infrastructure/Utilites/DatabaseHelper.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using OptixServe.Infrastructure.Configuration; | ||||||
|  | using OptixServe.Infrastructure.Data.CompiledModels; | ||||||
|  |  | ||||||
|  | namespace OptixServe.Infrastructure.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.Infrastructure")) | ||||||
|  |                 .UseModel(AppDbContextModel.Instance); | ||||||
|  |  | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             throw new NotImplementedException("Only SQLite database is currently supported"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										129
									
								
								OptixServe.Infrastructure/Utilites/DesignTimeDbContextFactory.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								OptixServe.Infrastructure/Utilites/DesignTimeDbContextFactory.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,129 @@ | |||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Design; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
|  | using OptixServe.Infrastructure.Configuration; | ||||||
|  | using OptixServe.Infrastructure.Data; | ||||||
|  |  | ||||||
|  | namespace OptixServe.Infrastructure.Utilites; | ||||||
|  |  | ||||||
|  | public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AppDbContext> | ||||||
|  | { | ||||||
|  |     private record CommandParseResult | ||||||
|  |     { | ||||||
|  |         public string? ConfigPath { get; set; } = null; | ||||||
|  |         public string? DataDir { get; set; } = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static CommandParseResult ParseArgs(string[] args) | ||||||
|  |     { | ||||||
|  |         var result = new CommandParseResult(); | ||||||
|  |         bool configSet = false; | ||||||
|  |         bool dataDirSet = false; | ||||||
|  |  | ||||||
|  |         for (int i = 0; i < args.Length; i++) | ||||||
|  |         { | ||||||
|  |             string arg = args[i]; | ||||||
|  |  | ||||||
|  |             if (arg == "--config" || arg == "-c") | ||||||
|  |             { | ||||||
|  |                 if (configSet) | ||||||
|  |                 { | ||||||
|  |                     throw new ArgumentException("The --config/-c option can only be specified once."); | ||||||
|  |                 } | ||||||
|  |                 if (i + 1 >= args.Length) | ||||||
|  |                 { | ||||||
|  |                     throw new ArgumentException("Missing value for --config/-c option."); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 result.ConfigPath = args[i + 1]; | ||||||
|  |                 configSet = true; | ||||||
|  |                 i++; | ||||||
|  |             } | ||||||
|  |             else if (arg == "--data-dir" || arg == "-d") | ||||||
|  |             { | ||||||
|  |                 if (dataDirSet) | ||||||
|  |                 { | ||||||
|  |                     throw new ArgumentException("The --data-dir/-d option can only be specified once."); | ||||||
|  |                 } | ||||||
|  |                 if (i + 1 >= args.Length) | ||||||
|  |                 { | ||||||
|  |                     throw new ArgumentException("Missing value for --data-dir/-d option."); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 result.DataDir = args[i + 1]; | ||||||
|  |                 dataDirSet = true; | ||||||
|  |                 i++; | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentException($"Unknown argument: {arg}"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Creates DbContext instance in Design-Time | ||||||
|  |     ///  | ||||||
|  |     /// <para>Custom arguments should be passed in <i>dotnet</i> command </para> | ||||||
|  |     ///  | ||||||
|  |     /// <b>Example:</b><br/> | ||||||
|  |     /// <example><c>dotnet ef database update -- -c ../data/appsettings.Development.json -d ../data/`</c></example> | ||||||
|  |     ///  | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="args"> | ||||||
|  |     ///     <list type="bullet"> | ||||||
|  |     ///         <item> | ||||||
|  |     ///             --config/-c: App configuration file to load with database connection settings. | ||||||
|  |     ///         </item> | ||||||
|  |     ///         <item> | ||||||
|  |     ///             --data-dir/-d: App data dir to work with. | ||||||
|  |     ///             Currently this only affects finding SQLite database when relative path | ||||||
|  |     ///             is specified in database settings. | ||||||
|  |     ///         </item> | ||||||
|  |     ///     </list> | ||||||
|  |     /// </param> | ||||||
|  |     /// <returns></returns> | ||||||
|  |     public AppDbContext CreateDbContext(string[] args) | ||||||
|  |     { | ||||||
|  |         var parsedArgs = ParseArgs(args); | ||||||
|  |  | ||||||
|  |         IConfigurationRoot configuration; | ||||||
|  |  | ||||||
|  |         if (!string.IsNullOrEmpty(parsedArgs.ConfigPath)) | ||||||
|  |         { | ||||||
|  |             var absConfigPath = Path.GetFullPath(parsedArgs.ConfigPath); | ||||||
|  |             var configDirectory = Path.GetDirectoryName(absConfigPath); | ||||||
|  |             var configFileName = Path.GetFileName(absConfigPath); | ||||||
|  |             // Use the provided config file path | ||||||
|  |             configuration = new ConfigurationBuilder() | ||||||
|  |                 .SetBasePath(configDirectory ?? "") | ||||||
|  |                 .AddJsonFile(configFileName, optional: false) | ||||||
|  |                 .Build(); | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             // Default search configuration path | ||||||
|  |             configuration = ConfigurationHelper.CreateDefaultBuilder().Build(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var dbSettings = configuration.GetSection("OptixServe:Infrastructure:Database").Get<DatabaseSettings>()!; | ||||||
|  |  | ||||||
|  |         // Resolve SQLite database if `--data-dir` is specified and `dbSettings.Host` is relative | ||||||
|  |         if (parsedArgs.DataDir != null) | ||||||
|  |         { | ||||||
|  |             if (dbSettings.Type == DatabaseType.Sqlite && !Path.IsPathFullyQualified(dbSettings.Host!)) | ||||||
|  |             { | ||||||
|  |                 var dbRelPath = Path.Combine(parsedArgs.DataDir, dbSettings.Host!); | ||||||
|  |                 var dbAbsPath = Path.GetFullPath(dbRelPath); | ||||||
|  |                 dbSettings.Host = dbAbsPath; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>(); | ||||||
|  |         DatabaseHelper.ConfigureDbContext(optionsBuilder, dbSettings); | ||||||
|  |  | ||||||
|  |         return new AppDbContext(optionsBuilder.Options); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -7,6 +7,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptixServe.Core", "OptixSer | |||||||
| EndProject | EndProject | ||||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptixServe.Api", "OptixServe.Api\OptixServe.Api.csproj", "{52559B29-A255-4BDC-8F2B-A984DEE69E7E}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptixServe.Api", "OptixServe.Api\OptixServe.Api.csproj", "{52559B29-A255-4BDC-8F2B-A984DEE69E7E}" | ||||||
| EndProject | EndProject | ||||||
|  | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptixServe.Infrastructure", "OptixServe.Infrastructure\OptixServe.Infrastructure.csproj", "{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}" | ||||||
|  | EndProject | ||||||
|  | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptixServe.Application", "OptixServe.Application\OptixServe.Application.csproj", "{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}" | ||||||
|  | EndProject | ||||||
| Global | Global | ||||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||||
| 		Debug|Any CPU = Debug|Any CPU | 		Debug|Any CPU = Debug|Any CPU | ||||||
| @ -41,6 +45,30 @@ Global | |||||||
| 		{52559B29-A255-4BDC-8F2B-A984DEE69E7E}.Release|x64.Build.0 = Release|Any CPU | 		{52559B29-A255-4BDC-8F2B-A984DEE69E7E}.Release|x64.Build.0 = Release|Any CPU | ||||||
| 		{52559B29-A255-4BDC-8F2B-A984DEE69E7E}.Release|x86.ActiveCfg = Release|Any CPU | 		{52559B29-A255-4BDC-8F2B-A984DEE69E7E}.Release|x86.ActiveCfg = Release|Any CPU | ||||||
| 		{52559B29-A255-4BDC-8F2B-A984DEE69E7E}.Release|x86.Build.0 = Release|Any CPU | 		{52559B29-A255-4BDC-8F2B-A984DEE69E7E}.Release|x86.Build.0 = Release|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Debug|x64.Build.0 = Debug|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Debug|x86.ActiveCfg = Debug|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Debug|x86.Build.0 = Debug|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Release|Any CPU.Build.0 = Release|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Release|x64.ActiveCfg = Release|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Release|x64.Build.0 = Release|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Release|x86.ActiveCfg = Release|Any CPU | ||||||
|  | 		{E90B4BE9-BCE3-48AC-B60E-1ADF2D328408}.Release|x86.Build.0 = Release|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Debug|x64.Build.0 = Debug|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Debug|x86.ActiveCfg = Debug|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Debug|x86.Build.0 = Debug|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Release|Any CPU.Build.0 = Release|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Release|x64.ActiveCfg = Release|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Release|x64.Build.0 = Release|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Release|x86.ActiveCfg = Release|Any CPU | ||||||
|  | 		{71D84B31-775B-4EF8-9D0B-411A8CE1CC3A}.Release|x86.Build.0 = Release|Any CPU | ||||||
| 	EndGlobalSection | 	EndGlobalSection | ||||||
| 	GlobalSection(SolutionProperties) = preSolution | 	GlobalSection(SolutionProperties) = preSolution | ||||||
| 		HideSolutionNode = FALSE | 		HideSolutionNode = FALSE | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user