diff --git a/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/Lab.Aws.S3.MinIOS3.csproj b/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/Lab.Aws.S3.MinIOS3.csproj new file mode 100644 index 00000000..547f14e9 --- /dev/null +++ b/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/Lab.Aws.S3.MinIOS3.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + diff --git a/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/UnitTest1.cs b/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/UnitTest1.cs new file mode 100644 index 00000000..1e2f17d0 --- /dev/null +++ b/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/UnitTest1.cs @@ -0,0 +1,86 @@ +using System.Collections.Concurrent; +using System.Reflection; +using Amazon; +using Amazon.S3; +using Amazon.S3.Model; + +namespace Lab.Aws.S3.MinIOS3; + +public class FieldTypeAssistant +{ + private static ConcurrentDictionary> s_fieldTypeList = new(); + + public static Dictionary GetStaticFieldValues() + { + var type = typeof(T); + var fieldTypeList = s_fieldTypeList; + if (fieldTypeList.TryGetValue(type, out var results)) + { + return results; + } + + var bindingFlags = BindingFlags.Public + | BindingFlags.Static + ; + results = new Dictionary(); + var fieldInfosInfos = type.GetFields(bindingFlags); + foreach (var fieldInfo in fieldInfosInfos) + { + var key = fieldInfo.Name; + var value = fieldInfo.GetValue(null); + + results.Add(value.ToString(), key); + } + + fieldTypeList.TryAdd(type, results); + return results; + } +} + +public class ProfileFieldNames +{ + public const string BB1Name = "BB1"; + + public const string BB2Name = "BB2"; + + private static readonly Lazy> s_valueDictionary = + new(FieldTypeAssistant.GetStaticFieldValues()); + + public static IReadOnlyDictionary GetValues() + { + return s_valueDictionary.Value; + } + + public static string GetValue(string key) + { + s_valueDictionary.Value.TryGetValue(key, out var value); + return value; + } +} + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void test() + { + var actual = ProfileFieldNames.GetValue("BB1"); + Assert.AreEqual("BB1Name", actual); + } + + [TestMethod] + public async Task 新增一個儲存桶() + { + var s3Config = new AmazonS3Config() + { + RegionEndpoint = RegionEndpoint.USEast1, + ServiceURL = "http://localhost:9000", + ForcePathStyle = true + }; + var s3Client = new AmazonS3Client(s3Config); + var response = await s3Client.PutBucketAsync(new PutBucketRequest + { + BucketName = "test-bucket", + }); + } +} \ No newline at end of file diff --git a/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/Usings.cs b/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/AWS/Lab.AwsS3/Lab.AwsS3.sln b/AWS/Lab.AwsS3/Lab.AwsS3.sln new file mode 100644 index 00000000..c4a97c99 --- /dev/null +++ b/AWS/Lab.AwsS3/Lab.AwsS3.sln @@ -0,0 +1,21 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Aws.S3.MinIOS3", "Lab.Aws.S3.MinIOS3\Lab.Aws.S3.MinIOS3.csproj", "{0FF96B3C-2410-4444-A113-FB2B80E4D940}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{17F77AC4-5953-4CD1-917D-28A4746AB759}" + ProjectSection(SolutionItems) = preProject + docker-compose.yaml = docker-compose.yaml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0FF96B3C-2410-4444-A113-FB2B80E4D940}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FF96B3C-2410-4444-A113-FB2B80E4D940}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FF96B3C-2410-4444-A113-FB2B80E4D940}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FF96B3C-2410-4444-A113-FB2B80E4D940}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/AWS/Lab.AwsS3/docker-compose.yaml b/AWS/Lab.AwsS3/docker-compose.yaml new file mode 100644 index 00000000..018232a7 --- /dev/null +++ b/AWS/Lab.AwsS3/docker-compose.yaml @@ -0,0 +1,17 @@ +version: "3.8" + +services: + s3-minio: + container_name: "s3-minio" + hostname: "minio" + image: minio/minio:latest + volumes: + - ./minio/data:/data + ports: + - "9000:9000" + - "9001:9001" + environment: + # 這裡的 key 要跟 .aws/credentials 裡的 key 名稱一樣,aws cli 才能正常的運作 + MINIO_ROOT_USER: "AKIAIOSFODNN7EXAMPLE" + MINIO_ROOT_PASSWORD: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + command: server --console-address :9001 /data \ No newline at end of file diff --git a/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.UnitTest/Lib.Middleware.OverrideResponse.UnitTest.csproj b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.UnitTest/Lib.Middleware.OverrideResponse.UnitTest.csproj new file mode 100644 index 00000000..b08e38ae --- /dev/null +++ b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.UnitTest/Lib.Middleware.OverrideResponse.UnitTest.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + diff --git a/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.UnitTest/OverrideResponseHandlerMiddlewareUnitTest.cs b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.UnitTest/OverrideResponseHandlerMiddlewareUnitTest.cs new file mode 100644 index 00000000..9527bcc9 --- /dev/null +++ b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.UnitTest/OverrideResponseHandlerMiddlewareUnitTest.cs @@ -0,0 +1,134 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Unicode; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lib.Middleware.OverrideResponse.UnitTest; + +[TestClass] +public class OverrideResponseHandlerMiddlewareUnitTest +{ + [TestMethod] + public async Task 不模糊訊息() + { + var expected = @"{""code"":""9527""}"; + + var serviceProvider = CreateServiceProvider(); + var jsonSerializerOptions = serviceProvider.GetService(); + var logger = serviceProvider.GetService>(); + var target = new OverrideResponseHandlerMiddleware(nextContext => + CreateFakeNextContext(nextContext, new { Code = "9527" }, StatusCodes.Status200OK)); + + var httpContext = new DefaultHttpContext + { + Response = { Body = new MemoryStream() } + }; + + await target.InvokeAsync(httpContext, logger, jsonSerializerOptions); + var response = httpContext.Response; + var stream = response.Body; + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + var actual = await new StreamReader(stream).ReadToEndAsync(); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public async Task 模糊化未授權訊息() + { + var expected = @"{""errorCode"":""NoAuthorization"",""errorMessage"":""Please contact your administrator""}"; + var httpContext = new DefaultHttpContext + { + Response = { Body = new MemoryStream() } + }; + var serviceProvider = CreateServiceProvider(); + var jsonSerializerOptions = serviceProvider.GetService(); + var logger = serviceProvider.GetService>(); + + var target = new OverrideResponseHandlerMiddleware(nextContext => + CreateFakeNextContext(nextContext, new + { + ErrorCode = "NoAuthorization", + ErrorMessage = "No permission" + }, StatusCodes.Status403Forbidden)); + + await target.InvokeAsync(httpContext, logger, jsonSerializerOptions); + + var response = httpContext.Response; + var stream = response.Body; + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + var actual = await new StreamReader(stream).ReadToEndAsync(); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public async Task 模糊化未驗證訊息() + { + var expected = @"{""errorCode"":""NoAuthentication"",""errorMessage"":""Please contact your administrator""}"; + var httpContext = new DefaultHttpContext + { + Response = { Body = new MemoryStream() } + }; + var serviceProvider = CreateServiceProvider(); + var jsonSerializerOptions = serviceProvider.GetService(); + var logger = serviceProvider.GetService>(); + + var target = new OverrideResponseHandlerMiddleware(nextContext => + CreateFakeNextContext(nextContext, new + { + ErrorCode = "NoAuthentication", + ErrorMessage = "Invalid userid or password" + }, StatusCodes.Status401Unauthorized)); + + await target.InvokeAsync(httpContext, logger, jsonSerializerOptions); + + var response = httpContext.Response; + var stream = response.Body; + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + var actual = await new StreamReader(stream).ReadToEndAsync(); + Assert.AreEqual(expected, actual); + } + + private static Task CreateFakeNextContext(HttpContext context, object detailFailure, int statusCode) + { + context.Response.StatusCode = statusCode; + context.Response.WriteAsJsonAsync(detailFailure); + return Task.CompletedTask; + } + + private static JsonSerializerOptions CreateJsonSerializerOptions() + { + return new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, + UnicodeRanges.CjkUnifiedIdeographs), + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + } + + private static IServiceProvider CreateServiceProvider() + { + var services = new ServiceCollection(); + services.AddSingleton(p => CreateJsonSerializerOptions()); + services.AddSingleton(p => LoggerFactory.Create(builder => { builder.AddConsole(); })); + services.AddSingleton(p => p.GetService().CreateLogger()); + return services.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.UnitTest/Usings.cs b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.UnitTest/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.UnitTest/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.sln b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.sln new file mode 100644 index 00000000..8868720c --- /dev/null +++ b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib.Middleware.OverrideResponse", "Lib.Middleware.OverrideResponse\Lib.Middleware.OverrideResponse.csproj", "{F696FEA1-4126-42F0-8D2F-6F7BE99DF418}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib.Middleware.OverrideResponse.UnitTest", "Lib.Middleware.OverrideResponse.UnitTest\Lib.Middleware.OverrideResponse.UnitTest.csproj", "{F5FD889F-037A-476E-B1F2-A01769A34674}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F696FEA1-4126-42F0-8D2F-6F7BE99DF418}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F696FEA1-4126-42F0-8D2F-6F7BE99DF418}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F696FEA1-4126-42F0-8D2F-6F7BE99DF418}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F696FEA1-4126-42F0-8D2F-6F7BE99DF418}.Release|Any CPU.Build.0 = Release|Any CPU + {F5FD889F-037A-476E-B1F2-A01769A34674}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5FD889F-037A-476E-B1F2-A01769A34674}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5FD889F-037A-476E-B1F2-A01769A34674}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5FD889F-037A-476E-B1F2-A01769A34674}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.csproj b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.csproj new file mode 100644 index 00000000..b6df6955 --- /dev/null +++ b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse/OverrideResponseHandlerMiddleware.cs b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse/OverrideResponseHandlerMiddleware.cs new file mode 100644 index 00000000..22de1636 --- /dev/null +++ b/AspNetCore/Lib.Middleware.OverrideResponse/Lib.Middleware.OverrideResponse/OverrideResponseHandlerMiddleware.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Lib.Middleware.OverrideResponse; + +public class OverrideResponseHandlerMiddleware +{ + private readonly RequestDelegate _next; + + public OverrideResponseHandlerMiddleware(RequestDelegate next) + { + this._next = next; + } + + public async Task InvokeAsync(HttpContext context, + ILogger logger, + JsonSerializerOptions jsonSerializerOptions) + { + var originalResponseBodyStream = context.Response.Body; + await using var newResponseBodyStream = new MemoryStream(); + context.Response.Body = newResponseBodyStream; + + await this._next(context); + + newResponseBodyStream.Seek(0, SeekOrigin.Begin); + var statusCode = context.Response.StatusCode; + var fuzzyBody = statusCode switch + { + 401 => CreateFuzzyBody("NoAuthentication"), + 403 => CreateFuzzyBody("NoAuthorization"), + _ => null + }; + + if (fuzzyBody != null) + { + var fuzzyData = JsonSerializer.Serialize(fuzzyBody, jsonSerializerOptions); + logger.LogInformation("Fuzzy data:{FuzzyData}", fuzzyData); + + var realData = await new StreamReader(newResponseBodyStream).ReadToEndAsync(); + logger.LogInformation("Read data:{RealData}", realData); + + context.Response.Body = originalResponseBodyStream; + await context.Response.WriteAsync(fuzzyData); + } + else + { + await newResponseBodyStream.CopyToAsync(originalResponseBodyStream); + context.Response.Body = originalResponseBodyStream; + } + } + + private static object CreateFuzzyBody(string failureCode) + { + return new + { + ErrorCode = failureCode, + ErrorMessage = "Please contact your administrator" + }; + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Controllers/ValuesController.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Controllers/ValuesController.cs new file mode 100644 index 00000000..a926f283 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Controllers/ValuesController.cs @@ -0,0 +1,79 @@ +using Lab.NETMiniProfiler.Infrastructure.EFCore5.EntityModel; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using StackExchange.Profiling; + +namespace Lab.NETMiniProfiler.ASPNetCore5.Controllers +{ + /// + /// Value Controller + /// + [Route("[controller]")] + [ApiController] + public class ValuesController : ControllerBase + { + private readonly IDbContextFactory _employeeDbContextFactory; + + public ValuesController(IDbContextFactory employeeDbContextFactory) + { + this._employeeDbContextFactory = employeeDbContextFactory; + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { + } + + /// + /// Get Api + /// + /// + + // GET api/values + [HttpGet] + public async Task Get(CancellationToken cancel = default) + { + using (MiniProfiler.Current.Step("查詢資料庫")) + { + await using var db = this._employeeDbContextFactory.CreateDbContext(); + return this.Ok(await db.Employees.AsTracking().ToListAsync(cancel)); + } + } + + // GET api/values/5 + [HttpGet("{id}")] + public ActionResult Get(int id) + { + return "value"; + } + + // POST api/values + [HttpPost] + public async Task Post(CancellationToken cancel = default) + { + using (MiniProfiler.Current.Step("異動資料庫")) + { + await using var db = this._employeeDbContextFactory.CreateDbContext(); + + var toDb = new Employee + { + Id = Guid.NewGuid(), + CreateAt = DateTimeOffset.Now, + CreateBy = Faker.Name.FullName(), + Age = Faker.RandomNumber.Next(1, 100), + Name = Faker.Name.Suffix(), + }; + db.Employees.Add(toDb); + await db.SaveChangesAsync(cancel); + return this.Ok(toDb); + } + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody] string value) + { + } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Lab.NETMiniProfiler.ASPNetCore5.csproj b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Lab.NETMiniProfiler.ASPNetCore5.csproj new file mode 100644 index 00000000..98d92d65 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Lab.NETMiniProfiler.ASPNetCore5.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + enable + enable + 10 + + + + + + + + + + + + + + + + + + + diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Program.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Program.cs new file mode 100644 index 00000000..0d2a45b3 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Program.cs @@ -0,0 +1,16 @@ +namespace Lab.NETMiniProfiler.ASPNetCore5 +{ + public class Program + { + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } + + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Properties/launchSettings.json b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Properties/launchSettings.json new file mode 100644 index 00000000..2c7f3c3c --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Properties/launchSettings.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:41185", + "sslPort": 44361 + } + }, + "profiles": { + "Lab.NETMiniProfiler.ASPNetCore5": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7186;http://localhost:5186", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "EMPLOYEE_DB_CONNECTION_STR": "Host=localhost;Port=5432;Database=member_service;Username=postgres;Password=guest", + "DB_TYPE": "postgresSQL", + "//EMPLOYEE_DB_CONNECTION_STR": "Data Source=localhost;Initial Catalog=EmployeeDb;Integrated Security=false;User ID=sa;Password=pass@w0rd1~;MultipleActiveResultSets=True;TrustServerCertificate=True", + "//DB_RTPE": "MsSQL" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "EMPLOYEE_DB_CONNECTION_STR": "Data Source=localhost;Initial Catalog=EmployeeDb;Integrated Security=false;User ID=sa;Password=pass@w0rd1~;MultipleActiveResultSets=True;TrustServerCertificate=True" + } + } + } +} diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Startup.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Startup.cs new file mode 100644 index 00000000..8d925b6b --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/Startup.cs @@ -0,0 +1,79 @@ +using System.Diagnostics; +using System.Reflection; +using Lab.NETMiniProfiler.Infrastructure.EFCore5; +using Lab.NETMiniProfiler.Infrastructure.EFCore5.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; + +namespace Lab.NETMiniProfiler.ASPNetCore5 +{ + public class Startup + { + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + { + this.Configuration = configuration; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + + //app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication1 v1")); + app.UseSwaggerUI(c => + { + c.RoutePrefix = "swagger"; + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + c.IndexStream = () => this.GetType() + .GetTypeInfo() + .Assembly + .GetManifestResourceStream("Lab.NETMiniProfiler.ASPNetCore5.index.html"); + }); + + app.UseMiniProfiler(); + } + + PreConnectionDb(app); + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication1", Version = "v1" }); + }); + + services.AddMiniProfiler(o => o.RouteBasePath = "/profiler") + .AddEntityFramework(); + services.AddAppEnvironment(); + services.AddEntityFramework(); + } + + private static void PreConnectionDb(IApplicationBuilder app) + { + var employeeDbContextFactory = + app.ApplicationServices.GetService>(); + var db = employeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + Debug.WriteLine("資料庫已連線"); + } + } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/appsettings.Development.json b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/appsettings.json b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/index.html b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/index.html new file mode 100644 index 00000000..49c7aa3d --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore5/index.html @@ -0,0 +1,99 @@ + + + + + + + + %(DocumentTitle) + + + + + + + %(HeadContent) + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Controllers/ValuesController.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Controllers/ValuesController.cs new file mode 100644 index 00000000..b9dd0977 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Controllers/ValuesController.cs @@ -0,0 +1,79 @@ +using Lab.NETMiniProfiler.Infrastructure.EFCore6.EntityModel; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using StackExchange.Profiling; + +namespace Lab.NETMiniProfiler.ASPNetCore6.Controllers +{ + /// + /// Value Controller + /// + [Route("[controller]")] + [ApiController] + public class ValuesController : ControllerBase + { + private readonly IDbContextFactory _employeeDbContextFactory; + + public ValuesController(IDbContextFactory employeeDbContextFactory) + { + this._employeeDbContextFactory = employeeDbContextFactory; + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { + } + + /// + /// Get Api + /// + /// + + // GET api/values + [HttpGet] + public async Task Get(CancellationToken cancel = default) + { + using (MiniProfiler.Current.Step("查詢資料庫")) + { + await using var db = await this._employeeDbContextFactory.CreateDbContextAsync(cancel); + return this.Ok(await db.Employees.AsTracking().ToListAsync(cancel)); + } + } + + // GET api/values/5 + [HttpGet("{id}")] + public ActionResult Get(int id) + { + return "value"; + } + + // POST api/values + [HttpPost] + public async Task Post(CancellationToken cancel = default) + { + using (MiniProfiler.Current.Step("異動資料庫")) + { + await using var db = await this._employeeDbContextFactory.CreateDbContextAsync(cancel); + + var toDb = new Employee + { + Id = Guid.NewGuid(), + CreateAt = DateTimeOffset.Now, + CreateBy = Faker.Name.FullName(), + Age = Faker.RandomNumber.Next(1, 100), + Name = Faker.Name.Suffix(), + }; + db.Employees.Add(toDb); + await db.SaveChangesAsync(cancel); + return this.Ok(toDb); + } + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody] string value) + { + } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Lab.NETMiniProfiler.ASPNetCore6.csproj b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Lab.NETMiniProfiler.ASPNetCore6.csproj new file mode 100644 index 00000000..de1b6b14 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Lab.NETMiniProfiler.ASPNetCore6.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Program.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Program.cs new file mode 100644 index 00000000..add4dbc4 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Program.cs @@ -0,0 +1,55 @@ +using System.Diagnostics; +using System.Reflection; +using Lab.NETMiniProfiler.Infrastructure.EFCore6; +using Lab.NETMiniProfiler.Infrastructure.EFCore6.EntityModel; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddMiniProfiler(o => o.RouteBasePath = "/profiler") + .AddEntityFramework(); +builder.Services.AddAppEnvironment(); +builder.Services.AddEntityFramework(); +var app = builder.Build(); +PreConnectionDb(app); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + // app.UseSwaggerUI(); + app.UseSwaggerUI(c => + { + c.RoutePrefix = "swagger"; + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + c.IndexStream = () => typeof(Program).GetTypeInfo() + .Assembly + .GetManifestResourceStream("Lab.NETMiniProfiler.ASPNetCore6.index.html"); + }); + app.UseMiniProfiler(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); +app.Run(); + +static void PreConnectionDb(IApplicationBuilder app) +{ + var employeeDbContextFactory = + app.ApplicationServices.GetService>(); + var db = employeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + Debug.WriteLine("資料庫已連線"); + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Properties/launchSettings.json b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Properties/launchSettings.json new file mode 100644 index 00000000..b6348197 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/Properties/launchSettings.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:30924", + "sslPort": 44345 + } + }, + "profiles": { + "Lab.NETMiniProfiler.ASPNetCore6": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7139;http://localhost:5139", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "EMPLOYEE_DB_CONNECTION_STR": "Host=localhost;Port=5432;Database=member_service;Username=postgres;Password=guest", + "DB_TYPE": "postgresSQL", + "//EMPLOYEE_DB_CONNECTION_STR": "Data Source=localhost;Initial Catalog=EmployeeDb;Integrated Security=false;User ID=sa;Password=pass@w0rd1~;MultipleActiveResultSets=True;TrustServerCertificate=True", + "//DB_RTPE": "MsSQL" + + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/appsettings.Development.json b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/appsettings.json b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/index.html b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/index.html new file mode 100644 index 00000000..49c7aa3d --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.ASPNetCore6/index.html @@ -0,0 +1,99 @@ + + + + + + + + %(DocumentTitle) + + + + + + + %(HeadContent) + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/AppDependencyInjectionExtensions.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/AppDependencyInjectionExtensions.cs new file mode 100644 index 00000000..e42e5a84 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/AppDependencyInjectionExtensions.cs @@ -0,0 +1,65 @@ +using Lab.NETMiniProfiler.Infrastructure.EFCore5.EntityModel; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + +namespace Lab.NETMiniProfiler.Infrastructure.EFCore5; +public static class AppDependencyInjectionExtensions +{ + public static void AddAppEnvironment(this IServiceCollection services) + { + services.AddLogging(builder => { builder.AddConsole(); }); + services.AddSingleton(); + } + + public static void AddEntityFramework(this IServiceCollection services) + { + // services.AddPooledDbContextFactory((provider, optionsBuilder) => + // { + // var option = provider.GetService(); + // var connectionString = option.EmployeeDbConnectionString; + // var loggerFactory = provider.GetService(); + // optionsBuilder.UseSqlServer(connectionString) + // .UseLoggerFactory(loggerFactory) + // ; + // }); + + services.AddPooledDbContextFactory((provider, optionsBuilder) => + { + // var mssqlOptions = optionsBuilder.Options.FindExtension(); + // var npgsqlOptions = optionsBuilder.Options.FindExtension(); + + var appOption = provider.GetService(); + var loggerFactory = provider.GetService(); + var connectionString = appOption.EmployeeDbConnectionString; + + + switch (appOption.DatabaseType) + { + case DatabaseType.MsSql: + optionsBuilder.UseSqlServer(connectionString) + .UseLoggerFactory(loggerFactory); + break; + case DatabaseType.PostgresSQL: + optionsBuilder.UseNpgsql( + connectionString, //只會呼叫一次 + builder => + builder.EnableRetryOnFailure( + 10, + TimeSpan.FromSeconds(30), + new List { "57P01" })) + + // .UseLazyLoadingProxies() + // .UseSnakeCaseNamingConvention() + .UseLoggerFactory(loggerFactory) + ; + break; + default: + throw new ArgumentOutOfRangeException(); + } + }); + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/AppEnvironmentOption.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/AppEnvironmentOption.cs new file mode 100644 index 00000000..4f5eca78 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/AppEnvironmentOption.cs @@ -0,0 +1,53 @@ +namespace Lab.NETMiniProfiler.Infrastructure.EFCore5; + +public enum DatabaseType +{ + MsSql = 1, + PostgresSQL = 2 +} + +public class AppEnvironmentOption +{ + public DatabaseType DatabaseType + { + get + { + if (this._databaseType.HasValue == false) + { + var variable = EnvironmentAssistant.GetEnvironmentVariable(this.DATABASE_TYPE); + if (Enum.TryParse(variable,true, out DatabaseType result)) + { + this._databaseType = result; + } + } + + return this._databaseType.Value; + } + set => this._databaseType = value; + } + + public string EmployeeDbConnectionString + { + get + { + if (string.IsNullOrWhiteSpace(this._employeeDbConnectionString)) + { + this._employeeDbConnectionString = + EnvironmentAssistant.GetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR); + } + + return this._employeeDbConnectionString; + } + set + { + this._employeeDbConnectionString = value; + Environment.SetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR, value); + } + } + + private readonly string DATABASE_TYPE = "DB_TYPE"; + private readonly string EMPLOYEE_DB_CONN_STR = "EMPLOYEE_DB_CONNECTION_STR"; + private DatabaseType? _databaseType; + + private string _employeeDbConnectionString; +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/Employee.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/Employee.cs new file mode 100644 index 00000000..51384037 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/Employee.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.NETMiniProfiler.Infrastructure.EFCore5.EntityModel +{ + [Table("Employee")] + public class Employee + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Id { get; set; } + + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + public string Remark { get; set; } + + [Required] + public DateTimeOffset CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + public virtual Identity Identity { get; set; } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/EmployeeDbContext.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..a98e18d5 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; + +namespace Lab.NETMiniProfiler.Infrastructure.EFCore5.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = { false }; + + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identities { get; set; } + + public virtual DbSet OrderHistories { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + if (s_migrated[0]) + { + return; + } + + lock (s_migrated) + { + if (s_migrated[0] == false) + { + var sqlOptions = options.FindExtension(); + if (sqlOptions != null) + { + Console.WriteLine( + $"EmployeeDbContext of connection string be '{sqlOptions.ConnectionString}'"); + } + + if (this.Database.CanConnect() == false) + { + this.Database.EnsureCreated(); + } + else + { + this.Database.Migrate(); + } + + s_migrated[0] = true; + } + } + } + + //管理索引 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(p => + { + p.HasKey(e => e.Id) + .IsClustered(false); + + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + + p.Property(p => p.Remark) + .IsRequired(false) + ; + }); + + modelBuilder.Entity(p => + { + p.HasKey(e => e.Employee_Id) + .IsClustered(false); + + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + }); + } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/Identity.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/Identity.cs new file mode 100644 index 00000000..8537c235 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/Identity.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.NETMiniProfiler.Infrastructure.EFCore5.EntityModel +{ + [Table("Identity")] + public class Identity + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Employee_Id { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Remark { get; set; } + + [Required] + public DateTimeOffset CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + [ForeignKey("Employee_Id")] + public virtual Employee Employee { get; set; } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/OrderHistory.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/OrderHistory.cs new file mode 100644 index 00000000..4da48bbe --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EntityModel/OrderHistory.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.NETMiniProfiler.Infrastructure.EFCore5.EntityModel +{ + [Table("OrderHistory")] + public class OrderHistory + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public Guid? Employee_Id { get; set; } + + public string Remark { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Product_Id { get; set; } + + public string Product_Name { get; set; } + + [Required] + public DateTime CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EnvironmentAssistant.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EnvironmentAssistant.cs new file mode 100644 index 00000000..32256cff --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/EnvironmentAssistant.cs @@ -0,0 +1,15 @@ +namespace Lab.NETMiniProfiler.Infrastructure.EFCore5; + +public class EnvironmentAssistant +{ + public static string GetEnvironmentVariable(string key) + { + var result = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrWhiteSpace(result)) + { + throw new Exception($"the key '{key}' not exist in environment variable"); + } + + return result; + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/Lab.NETMiniProfiler.Infrastructure.EFCore5.csproj b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/Lab.NETMiniProfiler.Infrastructure.EFCore5.csproj new file mode 100644 index 00000000..8f5315eb --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore5/Lab.NETMiniProfiler.Infrastructure.EFCore5.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + enable + enable + 10 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/AppDependencyInjectionExtensions.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/AppDependencyInjectionExtensions.cs new file mode 100644 index 00000000..7230b81e --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/AppDependencyInjectionExtensions.cs @@ -0,0 +1,49 @@ +using Lab.NETMiniProfiler.Infrastructure.EFCore6.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.NETMiniProfiler.Infrastructure.EFCore6; +public static class AppDependencyInjectionExtensions +{ + public static void AddAppEnvironment(this IServiceCollection services) + { + services.AddLogging(builder => { builder.AddConsole(); }); + services.AddSingleton(); + } + + public static void AddEntityFramework(this IServiceCollection services) + { + services.AddPooledDbContextFactory((provider, optionsBuilder) => + { + var appOption = provider.GetService(); + var loggerFactory = provider.GetService(); + var connectionString = appOption.EmployeeDbConnectionString; + + + switch (appOption.DatabaseType) + { + case DatabaseType.MsSql: + optionsBuilder.UseSqlServer(connectionString) + .UseLoggerFactory(loggerFactory); + break; + case DatabaseType.PostgresSQL: + optionsBuilder.UseNpgsql( + connectionString, //只會呼叫一次 + builder => + builder.EnableRetryOnFailure( + 10, + TimeSpan.FromSeconds(30), + new List { "57P01" })) + + // .UseLazyLoadingProxies() + // .UseSnakeCaseNamingConvention() + .UseLoggerFactory(loggerFactory) + ; + break; + default: + throw new ArgumentOutOfRangeException(); + } + }); + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/AppEnvironmentOption.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/AppEnvironmentOption.cs new file mode 100644 index 00000000..6614f3be --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/AppEnvironmentOption.cs @@ -0,0 +1,53 @@ +namespace Lab.NETMiniProfiler.Infrastructure.EFCore6; + +public enum DatabaseType +{ + MsSql = 1, + PostgresSQL = 2 +} + +public class AppEnvironmentOption +{ + public DatabaseType DatabaseType + { + get + { + if (this._databaseType.HasValue == false) + { + var variable = EnvironmentAssistant.GetEnvironmentVariable(this.DATABASE_TYPE); + if (Enum.TryParse(variable,true, out DatabaseType result)) + { + this._databaseType = result; + } + } + + return this._databaseType.Value; + } + set => this._databaseType = value; + } + + public string EmployeeDbConnectionString + { + get + { + if (string.IsNullOrWhiteSpace(this._employeeDbConnectionString)) + { + this._employeeDbConnectionString = + EnvironmentAssistant.GetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR); + } + + return this._employeeDbConnectionString; + } + set + { + this._employeeDbConnectionString = value; + Environment.SetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR, value); + } + } + + private readonly string DATABASE_TYPE = "DB_TYPE"; + private readonly string EMPLOYEE_DB_CONN_STR = "EMPLOYEE_DB_CONNECTION_STR"; + private DatabaseType? _databaseType; + + private string _employeeDbConnectionString; +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/Employee.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/Employee.cs new file mode 100644 index 00000000..1799f47d --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/Employee.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.NETMiniProfiler.Infrastructure.EFCore6.EntityModel +{ + [Table("Employee")] + public class Employee + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Id { get; set; } + + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + public string Remark { get; set; } + + [Required] + public DateTimeOffset CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + public virtual Identity Identity { get; set; } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/EmployeeDbContext.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..14beaacf --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; + +namespace Lab.NETMiniProfiler.Infrastructure.EFCore6.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = { false }; + + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identities { get; set; } + + public virtual DbSet OrderHistories { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + if (s_migrated[0]) + { + return; + } + + lock (s_migrated) + { + if (s_migrated[0] == false) + { + var sqlOptions = options.FindExtension(); + if (sqlOptions != null) + { + Console.WriteLine( + $"EmployeeDbContext of connection string be '{sqlOptions.ConnectionString}'"); + } + + if (this.Database.CanConnect() == false) + { + this.Database.EnsureCreated(); + } + else + { + this.Database.Migrate(); + } + + s_migrated[0] = true; + } + } + } + + //管理索引 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(p => + { + p.HasKey(e => e.Id) + .IsClustered(false); + + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + + p.Property(p => p.Remark) + .IsRequired(false) + ; + }); + + modelBuilder.Entity(p => + { + p.HasKey(e => e.Employee_Id) + .IsClustered(false); + + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + }); + } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/Identity.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/Identity.cs new file mode 100644 index 00000000..47f791e7 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/Identity.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.NETMiniProfiler.Infrastructure.EFCore6.EntityModel +{ + [Table("Identity")] + public class Identity + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Employee_Id { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Remark { get; set; } + + [Required] + public DateTimeOffset CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + [ForeignKey("Employee_Id")] + public virtual Employee Employee { get; set; } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/OrderHistory.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/OrderHistory.cs new file mode 100644 index 00000000..f582242b --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EntityModel/OrderHistory.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.NETMiniProfiler.Infrastructure.EFCore6.EntityModel +{ + [Table("OrderHistory")] + public class OrderHistory + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public Guid? Employee_Id { get; set; } + + public string Remark { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Product_Id { get; set; } + + public string Product_Name { get; set; } + + [Required] + public DateTime CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EnvironmentAssistant.cs b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EnvironmentAssistant.cs new file mode 100644 index 00000000..bc7c8f51 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/EnvironmentAssistant.cs @@ -0,0 +1,15 @@ +namespace Lab.NETMiniProfiler.Infrastructure.EFCore6; + +public class EnvironmentAssistant +{ + public static string GetEnvironmentVariable(string key) + { + var result = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrWhiteSpace(result)) + { + throw new Exception($"the key '{key}' not exist in environment variable"); + } + + return result; + } +} \ No newline at end of file diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/Lab.NETMiniProfiler.Infrastructure.EFCore6.csproj b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/Lab.NETMiniProfiler.Infrastructure.EFCore6.csproj new file mode 100644 index 00000000..2af7dc9f --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.Infrastructure.EFCore6/Lab.NETMiniProfiler.Infrastructure.EFCore6.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + 10 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.sln b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.sln new file mode 100644 index 00000000..5f6fca96 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/Lab.NETMiniProfiler.sln @@ -0,0 +1,56 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31911.196 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lab.NETMiniProfiler.ASPNetCore5", "Lab.NETMiniProfiler.ASPNetCore5\Lab.NETMiniProfiler.ASPNetCore5.csproj", "{8D4D97D5-C7F1-4BD7-9FE8-7A3766DB8D04}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.NETMiniProfiler.Infrastructure.EFCore5", "Lab.NETMiniProfiler.Infrastructure.EFCore5\Lab.NETMiniProfiler.Infrastructure.EFCore5.csproj", "{083D436C-B451-4BCE-8A97-E6E77B9F9A23}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8E1AD56E-E673-4533-B933-3712BD42BD4D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{BF639261-D8D3-4F57-8682-C0262A5AFE04}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.NETMiniProfiler.ASPNetCore6", "Lab.NETMiniProfiler.ASPNetCore6\Lab.NETMiniProfiler.ASPNetCore6.csproj", "{A677D49D-8088-44DD-9900-FC7694C30D70}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.NETMiniProfiler.Infrastructure.EFCore6", "Lab.NETMiniProfiler.Infrastructure.EFCore6\Lab.NETMiniProfiler.Infrastructure.EFCore6.csproj", "{D4CB746B-5711-4338-B716-763A86822134}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8D4D97D5-C7F1-4BD7-9FE8-7A3766DB8D04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D4D97D5-C7F1-4BD7-9FE8-7A3766DB8D04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D4D97D5-C7F1-4BD7-9FE8-7A3766DB8D04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D4D97D5-C7F1-4BD7-9FE8-7A3766DB8D04}.Release|Any CPU.Build.0 = Release|Any CPU + {083D436C-B451-4BCE-8A97-E6E77B9F9A23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {083D436C-B451-4BCE-8A97-E6E77B9F9A23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {083D436C-B451-4BCE-8A97-E6E77B9F9A23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {083D436C-B451-4BCE-8A97-E6E77B9F9A23}.Release|Any CPU.Build.0 = Release|Any CPU + {A677D49D-8088-44DD-9900-FC7694C30D70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A677D49D-8088-44DD-9900-FC7694C30D70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A677D49D-8088-44DD-9900-FC7694C30D70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A677D49D-8088-44DD-9900-FC7694C30D70}.Release|Any CPU.Build.0 = Release|Any CPU + {D4CB746B-5711-4338-B716-763A86822134}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4CB746B-5711-4338-B716-763A86822134}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4CB746B-5711-4338-B716-763A86822134}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4CB746B-5711-4338-B716-763A86822134}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1A39E53A-25EB-4546-9E76-DA1904FE5DCA} + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8D4D97D5-C7F1-4BD7-9FE8-7A3766DB8D04} = {8E1AD56E-E673-4533-B933-3712BD42BD4D} + {083D436C-B451-4BCE-8A97-E6E77B9F9A23} = {8E1AD56E-E673-4533-B933-3712BD42BD4D} + {A677D49D-8088-44DD-9900-FC7694C30D70} = {8E1AD56E-E673-4533-B933-3712BD42BD4D} + {D4CB746B-5711-4338-B716-763A86822134} = {8E1AD56E-E673-4533-B933-3712BD42BD4D} + EndGlobalSection +EndGlobal diff --git a/Benchmark/Lab.NETMiniProfiler/docker-compose.yml b/Benchmark/Lab.NETMiniProfiler/docker-compose.yml new file mode 100644 index 00000000..80c43a60 --- /dev/null +++ b/Benchmark/Lab.NETMiniProfiler/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3.8" + +services: + db-mssql: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=pass@w0rd1~ + ports: + - 1433:1433 + + db-postgres: + image: postgres:12-alpine + environment: + - POSTGRES_PASSWORD=guest + ports: + - 5432:5432 \ No newline at end of file diff --git a/CEF/Lab.Startup/WinFormCore30/WinFormCore30.csproj b/CEF/Lab.Startup/WinFormCore30/WinFormCore30.csproj index ae5a740d..755aa470 100644 --- a/CEF/Lab.Startup/WinFormCore30/WinFormCore30.csproj +++ b/CEF/Lab.Startup/WinFormCore30/WinFormCore30.csproj @@ -18,7 +18,7 @@ - + diff --git a/CEF/Lab.Startup/WinFormNet48/packages.config b/CEF/Lab.Startup/WinFormNet48/packages.config index 7ad8ca9f..db8da869 100644 --- a/CEF/Lab.Startup/WinFormNet48/packages.config +++ b/CEF/Lab.Startup/WinFormNet48/packages.config @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/Configuration/Lab.ConfigBind/Lab.ConfigBind.TestProject/Lab.ConfigBind.TestProject.csproj b/Configuration/Lab.ConfigBind/Lab.ConfigBind.TestProject/Lab.ConfigBind.TestProject.csproj new file mode 100644 index 00000000..67c4441e --- /dev/null +++ b/Configuration/Lab.ConfigBind/Lab.ConfigBind.TestProject/Lab.ConfigBind.TestProject.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + diff --git "a/Configuration/Lab.ConfigBind/Lab.ConfigBind.TestProject/\344\276\206\346\272\220\347\202\272\345\255\227\345\205\270\351\233\206\345\220\210.cs" "b/Configuration/Lab.ConfigBind/Lab.ConfigBind.TestProject/\344\276\206\346\272\220\347\202\272\345\255\227\345\205\270\351\233\206\345\220\210.cs" new file mode 100644 index 00000000..0565a7fe --- /dev/null +++ "b/Configuration/Lab.ConfigBind/Lab.ConfigBind.TestProject/\344\276\206\346\272\220\347\202\272\345\255\227\345\205\270\351\233\206\345\220\210.cs" @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.ConfigBind.TestProject; + +[TestClass] +public class 來源為字典集合 +{ + [TestMethod] + public void 綁定字典() + { + var source = new Dictionary + { + ["a:id"] = "9527", + ["a:profile:gender"] = "Male", + ["a:profile:age"] = "18", + ["a:profile:address"] = "Taipei", + ["b:id"] = "9528", + ["b:profile:gender"] = "Male", + ["b:profile:age"] = "19", + ["b:profile:address"] = "Taipei", + }; + + var builder = new ConfigurationBuilder(); + var configRoot = builder.AddEnvironmentVariables().Build(); + var member = configRoot.Get>(); + + Assert.AreEqual("9527", member["a"].Id); + Assert.AreEqual("9528", member["b"].Id); + } + + [TestMethod] + public void 綁定集合() + { + var source = new Dictionary + { + ["a:id"] = "9527", + ["a:profile:gender"] = "Male", + ["a:profile:age"] = "18", + ["a:profile:address"] = "Taipei", + ["b:id"] = "9528", + ["b:profile:gender"] = "Male", + ["b:profile:age"] = "19", + ["b:profile:address"] = "Taipei", + }; + + var builder = new ConfigurationBuilder(); + var configRoot = builder.AddInMemoryCollection(source).Build(); + var member = configRoot.Get>(); + + Assert.AreEqual("9527", member[0].Id); + Assert.AreEqual("9528", member[1].Id); + } + + [TestMethod] + public void 綁定複雜型別() + { + var source = new Dictionary + { + ["id"] = "9527", + ["profile:gender"] = "Male", + ["profile:age"] = "18", + ["profile:address"] = "Taipei", + }; + + var builder = new ConfigurationBuilder(); + var configRoot = builder.AddInMemoryCollection(source).Build(); + var member = configRoot.Get(); + + Assert.AreEqual("9527", member.Id); + Assert.AreEqual(18, member.Profile.Age); + Assert.AreEqual("Taipei", member.Profile.Address); + Assert.AreEqual(Gender.Male, member.Profile.Gender); + } + + private enum Gender + { + Male, + Female + } + + private class Member + { + public string Id { get; set; } + + public Profile Profile { get; set; } + } + + private class Profile + { + public Gender? Gender { get; set; } + + public int? Age { get; set; } + + public string Address { get; set; } + } +} \ No newline at end of file diff --git "a/Configuration/Lab.ConfigBind/Lab.ConfigBind.TestProject/\344\276\206\346\272\220\347\202\272\347\222\260\345\242\203\350\256\212\346\225\270.cs" "b/Configuration/Lab.ConfigBind/Lab.ConfigBind.TestProject/\344\276\206\346\272\220\347\202\272\347\222\260\345\242\203\350\256\212\346\225\270.cs" new file mode 100644 index 00000000..a9c53ad4 --- /dev/null +++ "b/Configuration/Lab.ConfigBind/Lab.ConfigBind.TestProject/\344\276\206\346\272\220\347\202\272\347\222\260\345\242\203\350\256\212\346\225\270.cs" @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.ConfigBind.TestProject; + +[TestClass] +public class 來源為環境變數 +{ + [TestMethod] + public void 綁定字典() + { + Environment.SetEnvironmentVariable("a:id", "9527"); + Environment.SetEnvironmentVariable("a:profile:gender", "Male"); + Environment.SetEnvironmentVariable("a:profile:age", "18"); + Environment.SetEnvironmentVariable("a:profile:address", "Taipei"); + Environment.SetEnvironmentVariable("b:id", "9528"); + Environment.SetEnvironmentVariable("b:profile:gender", "Male"); + Environment.SetEnvironmentVariable("b:profile:age", "19"); + Environment.SetEnvironmentVariable("b:profile:address", "Taipei"); + var builder = new ConfigurationBuilder(); + var configRoot = builder.AddEnvironmentVariables() + .Build(); + var member = configRoot.Get>(); + + Assert.AreEqual("9527", member["a"].Id); + Assert.AreEqual("9528", member["b"].Id); + } + + [TestMethod] + public void 綁定集合() + { + Environment.SetEnvironmentVariable("a:id", "9527"); + Environment.SetEnvironmentVariable("a:profile:gender", "Male"); + Environment.SetEnvironmentVariable("a:profile:age", "18"); + Environment.SetEnvironmentVariable("a:profile:address", "Taipei"); + Environment.SetEnvironmentVariable("b:id", "9528"); + Environment.SetEnvironmentVariable("b:profile:gender", "Male"); + Environment.SetEnvironmentVariable("b:profile:age", "19"); + Environment.SetEnvironmentVariable("b:profile:address", "Taipei"); + var builder = new ConfigurationBuilder(); + var configRoot = builder.AddEnvironmentVariables() + .Build(); + var member = configRoot.Get>(); + + Assert.AreEqual("9527", member[0].Id); + Assert.AreEqual("9528", member[1].Id); + } + + [TestMethod] + public void 綁定複雜型別() + { + Environment.SetEnvironmentVariable("id", "9527"); + Environment.SetEnvironmentVariable("profile:gender", "Male"); + Environment.SetEnvironmentVariable("profile:age", "18"); + Environment.SetEnvironmentVariable("profile:address", "Taipei"); + + var builder = new ConfigurationBuilder(); + var configRoot = builder.AddEnvironmentVariables() + .Build(); + var member = configRoot.Get(); + Assert.AreEqual("9527", member.Id); + Assert.AreEqual(18, member.Profile.Age); + Assert.AreEqual("Taipei", member.Profile.Address); + Assert.AreEqual(Gender.Male, member.Profile.Gender); + } + + private enum Gender + { + Male, + Female + } + + private class Member + { + public string Id { get; set; } + + public Profile Profile { get; set; } + } + + private class Profile + { + public Gender? Gender { get; set; } + + public int? Age { get; set; } + + public string Address { get; set; } + } +} \ No newline at end of file diff --git a/Configuration/Lab.ConfigBind/Lab.ConfigBind.sln b/Configuration/Lab.ConfigBind/Lab.ConfigBind.sln new file mode 100644 index 00000000..10724763 --- /dev/null +++ b/Configuration/Lab.ConfigBind/Lab.ConfigBind.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ConfigBind.TestProject", "Lab.ConfigBind.TestProject\Lab.ConfigBind.TestProject.csproj", "{3733D824-84BE-4993-A321-7DECC340FA64}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3733D824-84BE-4993-A321-7DECC340FA64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3733D824-84BE-4993-A321-7DECC340FA64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3733D824-84BE-4993-A321-7DECC340FA64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3733D824-84BE-4993-A321-7DECC340FA64}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.TestProject/Lab.EnvFileConfig.TestProject.csproj b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.TestProject/Lab.EnvFileConfig.TestProject.csproj new file mode 100644 index 00000000..03a75833 --- /dev/null +++ b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.TestProject/Lab.EnvFileConfig.TestProject.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + + + Always + + + + diff --git a/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.TestProject/UnitTest1.cs b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.TestProject/UnitTest1.cs new file mode 100644 index 00000000..27513906 --- /dev/null +++ b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.TestProject/UnitTest1.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.EnvFileConfig.TestProject; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void 讀取ENV檔案() + { + var configRoot = new ConfigurationBuilder() + + // .AddJsonFile("appSettings.json") + .AddEnvFile("secret.env") + .Build() + ; + var section = configRoot.GetSection("SQL_SERVER_CS"); + Console.WriteLine($"Value = {section.Value}"); + } + + [TestMethod] + public void 讀取ENV檔案後綁定() + { + var configRoot = new ConfigurationBuilder() + .AddEnvFile("secret.env") + .Build() + ; + var appSetting = configRoot.Get(); + Assert.AreEqual("foo-bar", appSetting.SQL_SERVER_CS); + Assert.AreEqual("localhost:6379", appSetting.REDIS_ENDPOINT); + } +} \ No newline at end of file diff --git a/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.TestProject/secret.env b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.TestProject/secret.env new file mode 100644 index 00000000..fc2d45ae --- /dev/null +++ b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.TestProject/secret.env @@ -0,0 +1,2 @@ +SQL_SERVER_CS=foo-bar +REDIS_ENDPOINT=localhost:6379 \ No newline at end of file diff --git a/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.sln b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.sln new file mode 100644 index 00000000..7a85e196 --- /dev/null +++ b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.EnvFileConfig", "Lab.EnvFileConfig\Lab.EnvFileConfig.csproj", "{2982F90A-3127-4E7E-8DC3-44512C0CE1E2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.EnvFileConfig.TestProject", "Lab.EnvFileConfig.TestProject\Lab.EnvFileConfig.TestProject.csproj", "{6C8009BE-E88B-4A97-9D44-AE753BEBE2E4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2982F90A-3127-4E7E-8DC3-44512C0CE1E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2982F90A-3127-4E7E-8DC3-44512C0CE1E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2982F90A-3127-4E7E-8DC3-44512C0CE1E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2982F90A-3127-4E7E-8DC3-44512C0CE1E2}.Release|Any CPU.Build.0 = Release|Any CPU + {6C8009BE-E88B-4A97-9D44-AE753BEBE2E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C8009BE-E88B-4A97-9D44-AE753BEBE2E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C8009BE-E88B-4A97-9D44-AE753BEBE2E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C8009BE-E88B-4A97-9D44-AE753BEBE2E4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/AppSetting.cs b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/AppSetting.cs new file mode 100644 index 00000000..78d54413 --- /dev/null +++ b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/AppSetting.cs @@ -0,0 +1,8 @@ +namespace Lab.EnvFileConfig; + +public class AppSetting +{ + public string SQL_SERVER_CS { get; set; } + + public string REDIS_ENDPOINT { get; set; } +} \ No newline at end of file diff --git a/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/EnvFileConfigurationExtensions.cs b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/EnvFileConfigurationExtensions.cs new file mode 100644 index 00000000..24143051 --- /dev/null +++ b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/EnvFileConfigurationExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Configuration; + +namespace Lab.EnvFileConfig +{ + public static class EnvFileConfigurationExtensions + { + public static IConfigurationBuilder AddEnvFile(this IConfigurationBuilder builder, string envFile) + { + var source = new EnvFileConfigurationSource(envFile); + builder.Add(source); + builder.AddEnvironmentVariables(); + return builder; + } + } +} \ No newline at end of file diff --git a/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/EnvFileConfigurationProvider.cs b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/EnvFileConfigurationProvider.cs new file mode 100644 index 00000000..4be70094 --- /dev/null +++ b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/EnvFileConfigurationProvider.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Configuration; + +namespace Lab.EnvFileConfig +{ + public class EnvFileConfigurationProvider : ConfigurationProvider + { + private readonly string _envFile; + + public EnvFileConfigurationProvider(string envFile) + { + this._envFile = envFile; + } + + public override void Load() + { + if (!File.Exists(this._envFile)) + { + return; + } + + foreach (var line in File.ReadAllLines(this._envFile)) + { + var parts = line.Split( + '=', + StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length != 2) + { + continue; + } + + Environment.SetEnvironmentVariable(parts[0], parts[1]); + } + } + } +} \ No newline at end of file diff --git a/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/EnvFileConfigurationSource.cs b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/EnvFileConfigurationSource.cs new file mode 100644 index 00000000..cc0c8e40 --- /dev/null +++ b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/EnvFileConfigurationSource.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Configuration; + +namespace Lab.EnvFileConfig +{ + public class EnvFileConfigurationSource : IConfigurationSource + { + private readonly string _envFile; + + public EnvFileConfigurationSource(string envFile) + { + this._envFile = envFile; + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new EnvFileConfigurationProvider(this._envFile); + } + } +} \ No newline at end of file diff --git a/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/Lab.EnvFileConfig.csproj b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/Lab.EnvFileConfig.csproj new file mode 100644 index 00000000..48c93535 --- /dev/null +++ b/Configuration/Lab.EnvFileConfig/Lab.EnvFileConfig/Lab.EnvFileConfig.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + enable + enable + + + + + + + + + + diff --git a/Configuration/Lab.Environment/.gitignore b/Configuration/Lab.Environment/.gitignore new file mode 100644 index 00000000..c027a961 --- /dev/null +++ b/Configuration/Lab.Environment/.gitignore @@ -0,0 +1 @@ +secrets.env \ No newline at end of file diff --git a/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET48/Lab.Environment.ConsoleApp.NET48.csproj b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET48/Lab.Environment.ConsoleApp.NET48.csproj new file mode 100644 index 00000000..099fd9b8 --- /dev/null +++ b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET48/Lab.Environment.ConsoleApp.NET48.csproj @@ -0,0 +1,55 @@ + + + + + Debug + AnyCPU + {BB8D3ED0-BF9F-4910-9231-717DD0577FB5} + Exe + Properties + Lab.Environment.ConsoleApp.NET48 + Lab.Environment.ConsoleApp.NET48 + v4.8 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + diff --git a/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET48/Program.cs b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET48/Program.cs new file mode 100644 index 00000000..e6a16b3a --- /dev/null +++ b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET48/Program.cs @@ -0,0 +1,19 @@ + +using System; + +namespace Lab.Environment.ConsoleApp.NET48 +{ + internal class Program + { + public static void Main(string[] args) + { + var appEnv = System.Environment.GetEnvironmentVariable("APP_ENV"); + var scoopPath = System.Environment.GetEnvironmentVariable("scoop"); + + if (string.IsNullOrWhiteSpace(appEnv) == false) + { + Console.WriteLine(appEnv); + } + } + } +} \ No newline at end of file diff --git a/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET48/Properties/AssemblyInfo.cs b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET48/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..814f8719 --- /dev/null +++ b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET48/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Lab.Environment.ConsoleApp.NET48")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Lab.Environment.ConsoleApp.NET48")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("BB8D3ED0-BF9F-4910-9231-717DD0577FB5")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET6/Lab.Environment.ConsoleApp.NET6.csproj b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET6/Lab.Environment.ConsoleApp.NET6.csproj new file mode 100644 index 00000000..b9de0634 --- /dev/null +++ b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET6/Lab.Environment.ConsoleApp.NET6.csproj @@ -0,0 +1,10 @@ + + + + Exe + net6.0 + enable + enable + + + diff --git a/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET6/Program.cs b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET6/Program.cs new file mode 100644 index 00000000..f6ff2edf --- /dev/null +++ b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET6/Program.cs @@ -0,0 +1,5 @@ +using System; + +var appEnv = Environment.GetEnvironmentVariable("APP_ENV"); +var scoopPath = Environment.GetEnvironmentVariable("scoop"); +Console.ReadKey(); \ No newline at end of file diff --git a/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET6/Properties/launchSettings.json b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET6/Properties/launchSettings.json new file mode 100644 index 00000000..e1fbded0 --- /dev/null +++ b/Configuration/Lab.Environment/Lab.Environment.ConsoleApp.NET6/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Lab.Environment.ConsoleApp.NET6": { + "commandName": "Project", + "environmentVariables": { + } + } + } +} diff --git a/Configuration/Lab.Environment/Lab.Environment.ps1 b/Configuration/Lab.Environment/Lab.Environment.ps1 new file mode 100644 index 00000000..1fd4ffcf --- /dev/null +++ b/Configuration/Lab.Environment/Lab.Environment.ps1 @@ -0,0 +1,2 @@ +$Env:APP_ENV = "QA" +./Lab.Environment.sln \ No newline at end of file diff --git a/Configuration/Lab.Environment/Lab.Environment.sln b/Configuration/Lab.Environment/Lab.Environment.sln new file mode 100644 index 00000000..291bd670 --- /dev/null +++ b/Configuration/Lab.Environment/Lab.Environment.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Environment.ConsoleApp.NET48", "Lab.Environment.ConsoleApp.NET48\Lab.Environment.ConsoleApp.NET48.csproj", "{BB8D3ED0-BF9F-4910-9231-717DD0577FB5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Environment.ConsoleApp.NET6", "Lab.Environment.ConsoleApp.NET6\Lab.Environment.ConsoleApp.NET6.csproj", "{5033EB4D-63B9-4F17-9FDD-1A81E174F3C9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{6ACD7D50-37E9-4003-B8BE-17FC3724B567}" + ProjectSection(SolutionItems) = preProject + Taskfile.yml = Taskfile.yml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BB8D3ED0-BF9F-4910-9231-717DD0577FB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB8D3ED0-BF9F-4910-9231-717DD0577FB5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB8D3ED0-BF9F-4910-9231-717DD0577FB5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB8D3ED0-BF9F-4910-9231-717DD0577FB5}.Release|Any CPU.Build.0 = Release|Any CPU + {5033EB4D-63B9-4F17-9FDD-1A81E174F3C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5033EB4D-63B9-4F17-9FDD-1A81E174F3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5033EB4D-63B9-4F17-9FDD-1A81E174F3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5033EB4D-63B9-4F17-9FDD-1A81E174F3C9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Configuration/Lab.Environment/Taskfile.yml b/Configuration/Lab.Environment/Taskfile.yml new file mode 100644 index 00000000..b97d93b8 --- /dev/null +++ b/Configuration/Lab.Environment/Taskfile.yml @@ -0,0 +1,26 @@ +version: "3" +env: + GREETING: Hey, there! +dotenv: ["secrets.env"] +vars: + PATH: "/mnt/c/Users/Yao Chang Yu/scoop/apps/Rider-EAP/current/IDE/bin/" + #PATH: "C:\Users\Yao Chang Yu\scoop\apps\Rider-EAP\2021.3-EAP9-213.5744.160\IDE\bin\" +tasks: + print-os: + cmds: + - echo '{{OS}} {{ARCH}}' + - echo '{{if eq OS "windows"}}windows-command{{else}}unix-command{{end}}' + # This will be path/to/file on Unix but path\to\file on Windows + - echo '{{fromSlash "path/to/file"}}' + - echo '{{fromSlash "/mnt/c/Users/Yao Chang Yu/scoop/apps/Rider-EAP/current/IDE/bin/"}}' + greet: + desc: greet + cmds: + - echo $GREETING + rider: + desc: Rider + dir: "/mnt/c/Users/Yao Chang Yu/scoop/apps/Rider-EAP/current/IDE/bin/" + cmds: + - rider64.exe + env: + Url: http://localhost:9527 \ No newline at end of file diff --git a/Configuration/Lab.Environment/global.json b/Configuration/Lab.Environment/global.json new file mode 100644 index 00000000..f443bd42 --- /dev/null +++ b/Configuration/Lab.Environment/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/AspNetCore3.csproj b/Configuration/NetCore/Lab.Config/AspNetCore3/AspNetCore3.csproj index dceedada..24827e37 100644 --- a/Configuration/NetCore/Lab.Config/AspNetCore3/AspNetCore3.csproj +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/AspNetCore3.csproj @@ -1,22 +1,29 @@ - - netcoreapp3.1 - + + netcoreapp3.1 + Debug;Release;QA + AnyCPU + - - - + + true + false + - - - + + + - - - Always - - + + + + + + + Always + + diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/Controllers/DefaultController.cs b/Configuration/NetCore/Lab.Config/AspNetCore3/Controllers/DefaultController.cs new file mode 100644 index 00000000..170d017e --- /dev/null +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/Controllers/DefaultController.cs @@ -0,0 +1,38 @@ +using Lab.Infra; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace AspNetCore3.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DefaultController : ControllerBase + { + [Route("options/appsettings")] + public IActionResult Get() + { + var serviceProvider = this.HttpContext.RequestServices; + var options = serviceProvider.GetService>(); + return this.Ok(options?.Value); + } + + [Route("monitor/players/{id}")] + public IActionResult GetMonitorPlayer(int id) + { + var serviceProvider = this.HttpContext.RequestServices; + var playerOption = serviceProvider.GetService>(); + var player = playerOption.Get($"Player{id}"); + return this.Ok(player); + } + + [Route("snapshot/players/{id}")] + public IActionResult GetSnapshotPlayer(int id) + { + var serviceProvider = this.HttpContext.RequestServices; + var playerOption = serviceProvider.GetService>(); + var player = playerOption.Get($"Player{id}"); + return this.Ok(player); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/Controllers/WeatherForecastController.cs b/Configuration/NetCore/Lab.Config/AspNetCore3/Controllers/WeatherForecastController.cs index 2fb9663f..55424a39 100644 --- a/Configuration/NetCore/Lab.Config/AspNetCore3/Controllers/WeatherForecastController.cs +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/Controllers/WeatherForecastController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Lab.Infra; @@ -19,32 +19,32 @@ public class WeatherForecastController : ControllerBase }; private readonly ILogger _logger; - private AppSetting _appSetting; - private IConfiguration _config; - private Player _player1; - private Player _player2; + private AppSetting _appSetting; + private IConfiguration _config; + private Player _player1; + private Player _player2; // TODO:依賴 AppSetting - public WeatherForecastController(AppSetting appSetting) - { - this._appSetting = appSetting; - } + // public WeatherForecastController(AppSetting appSetting) + // { + // this._appSetting = appSetting; + // } // TODO:依賴 IOptions - //public WeatherForecastController(IOptions options) - //{ - // try - // { - // this._appSetting = options.Value; - // } - // catch (OptionsValidationException ex) - // { - // foreach (var failure in ex.Failures) - // { - // Console.WriteLine(failure); - // } - // } - //} + public WeatherForecastController(IOptions options) + { + try + { + this._appSetting = options.Value; + } + catch (OptionsValidationException ex) + { + foreach (var failure in ex.Failures) + { + Console.WriteLine(failure); + } + } + } // TODO:依賴 IOptionsSnapshot //public WeatherForecastController(IOptionsSnapshot options) @@ -66,11 +66,13 @@ public WeatherForecastController(AppSetting appSetting) // this._config = config; //} - //public WeatherForecastController(IOptions options, IConfiguration config) - //{ - // this._config = config; - // this._appSetting = options.Value; - //} + // public WeatherForecastController(IOptions options, IConfiguration config) + // { + // this._config = config; + // var appSetting = new AppSetting(); + // config.Bind(appSetting); + // this._appSetting = options.Value; + // } //public WeatherForecastController(ILogger logger) //{ @@ -83,9 +85,9 @@ public IEnumerable Get() var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { - Date = DateTime.Now.AddDays(index), + Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] + Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); } diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/Program.cs b/Configuration/NetCore/Lab.Config/AspNetCore3/Program.cs index c9edcdf5..242d45b9 100644 --- a/Configuration/NetCore/Lab.Config/AspNetCore3/Program.cs +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/Program.cs @@ -1,29 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; namespace AspNetCore3 { public class Program { + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureAppConfiguration(p => + { + p.AddJsonFile("appsettings.json", false, false); + }); + webBuilder.UseStartup(); + + // webBuilder.UseStartup(); + // webBuilder.UseStartup(); + // webBuilder.UseStartup(); + // webBuilder.UseStartup(); + }); + } + public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - //webBuilder.UseStartup(); - //webBuilder.UseStartup(); - //webBuilder.UseStartup(); - webBuilder.UseStartup(); - }); } -} +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/Properties/launchSettings.json b/Configuration/NetCore/Lab.Config/AspNetCore3/Properties/launchSettings.json index 886261e3..546f5d39 100644 --- a/Configuration/NetCore/Lab.Config/AspNetCore3/Properties/launchSettings.json +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/Properties/launchSettings.json @@ -13,6 +13,7 @@ "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "weatherforecast", + "//launchUrl": "default", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/ServiceCollectionEx.cs b/Configuration/NetCore/Lab.Config/AspNetCore3/ServiceCollectionEx.cs index 1134ef0e..a1adff51 100644 --- a/Configuration/NetCore/Lab.Config/AspNetCore3/ServiceCollectionEx.cs +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/ServiceCollectionEx.cs @@ -1,35 +1,36 @@ -using System; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; +// using System; +// using Microsoft.Extensions.Configuration; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace AspNetCore3 +// { +// public static class ServiceCollectionEx +// { +// /// +// /// Inject AddSingleton +// /// +// /// +// /// +// /// +// /// +// public static TConfig Configure(this IServiceCollection services, IConfiguration configuration) +// where TConfig : class, new() +// { +// if (services == null) +// { +// throw new ArgumentNullException(nameof(services)); +// } +// +// if (configuration == null) +// { +// throw new ArgumentNullException(nameof(configuration)); +// } +// +// var config = Activator.CreateInstance(); +// configuration.Bind(config); +// services.AddSingleton(config); +// return config; +// } +// } +// } -namespace AspNetCore3 -{ - public static class ServiceCollectionEx - { - /// - /// Inject AddSingleton - /// - /// - /// - /// - /// - public static TConfig Configure(this IServiceCollection services, IConfiguration configuration) - where TConfig : class, new() - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - var config = Activator.CreateInstance(); - configuration.Bind(config); - services.AddSingleton(config); - return config; - } - } -} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/Startup.cs b/Configuration/NetCore/Lab.Config/AspNetCore3/Startup.cs index d4ba7974..d4468799 100644 --- a/Configuration/NetCore/Lab.Config/AspNetCore3/Startup.cs +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/Startup.cs @@ -37,6 +37,28 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) public void ConfigureServices(IServiceCollection services) { services.AddControllers(); + + //驗證 AppSetting + services.AddOptions() + .ValidateDataAnnotations() + .Validate(p => + { + if (p.AllowedHosts == null) + { + return false; + } + + return true; + }, "AllowedHosts must be value"); // Failure message. + + //注入 Options 和完整 IConfiguration + services.Configure(this.Configuration); + + //注入 Options 和 Configuration Section Name + // services.Configure("Player1", this.Configuration.GetSection("Player1")); + // services.Configure("Player2", this.Configuration.GetSection("Player2")); + // services.Configure("Player3", this.Configuration.GetSection("Player3")); + // services.Configure("ConnectionStrings", this.Configuration.GetSection("ConnectionStrings")); } } } \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/Startup_InjectionIAppSetting.cs b/Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionAppSetting.cs similarity index 73% rename from Configuration/NetCore/Lab.Config/AspNetCore3/Startup_InjectionIAppSetting.cs rename to Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionAppSetting.cs index 59fd68f8..311eda8e 100644 --- a/Configuration/NetCore/Lab.Config/AspNetCore3/Startup_InjectionIAppSetting.cs +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionAppSetting.cs @@ -7,11 +7,11 @@ namespace AspNetCore3 { - public class Startup_InjectionIAppSetting + public class StartupInjectionAppSetting { public IConfiguration Configuration { get; } - public Startup_InjectionIAppSetting(IConfiguration configuration) + public StartupInjectionAppSetting(IConfiguration configuration) { this.Configuration = configuration; } @@ -38,12 +38,14 @@ public void ConfigureServices(IServiceCollection services) { services.AddControllers(); - //var appSetting = new AppSetting(); - //this.Configuration.Bind(appSetting); - - ////`J AppSetting - //services.AddSingleton(appSetting); - services.Configure(this.Configuration); + //注入 AppSetting + services.AddSingleton(provider => + { + //lazy load + var appSetting = new AppSetting(); + this.Configuration.Bind(appSetting); + return appSetting; + }); } } } \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionIOptionsMonitor.cs b/Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionIOptionsMonitor.cs new file mode 100644 index 00000000..5a43db3c --- /dev/null +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionIOptionsMonitor.cs @@ -0,0 +1,51 @@ +using Lab.Infra; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace AspNetCore3 +{ + public class StartupInjectionOptionsMonitor + { + public IConfiguration Configuration { get; } + + public StartupInjectionOptionsMonitor(IConfiguration configuration) + { + this.Configuration = configuration; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + //注入 Options 和完整 IConfiguration + services.Configure(this.Configuration); + + //注入 Options 和 Configuration Section Name + services.Configure(this.Configuration); + services.Configure("Player1", this.Configuration.GetSection("Player1")); + services.Configure("Player2", this.Configuration.GetSection("Player2")); + services.Configure("Player3", this.Configuration.GetSection("Player3")); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/Startup_InjectionIOptions.cs b/Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionOptions.cs similarity index 84% rename from Configuration/NetCore/Lab.Config/AspNetCore3/Startup_InjectionIOptions.cs rename to Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionOptions.cs index 716c5e74..f953b4ce 100644 --- a/Configuration/NetCore/Lab.Config/AspNetCore3/Startup_InjectionIOptions.cs +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionOptions.cs @@ -7,11 +7,11 @@ namespace AspNetCore3 { - public class Startup_InjectionIOptions + public class StartupInjectionOptions { public IConfiguration Configuration { get; } - public Startup_InjectionIOptions(IConfiguration configuration) + public StartupInjectionOptions(IConfiguration configuration) { this.Configuration = configuration; } @@ -38,10 +38,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddControllers(); - //`J IOptions - services.AddOptions(); - - //`J IConfiguration + //注入 Options 和完整 IConfiguration services.Configure(this.Configuration); } } diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/Startup_InjectionIOptionsSnapshot.cs b/Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionOptionsSnapshot.cs similarity index 78% rename from Configuration/NetCore/Lab.Config/AspNetCore3/Startup_InjectionIOptionsSnapshot.cs rename to Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionOptionsSnapshot.cs index ee727426..7d983108 100644 --- a/Configuration/NetCore/Lab.Config/AspNetCore3/Startup_InjectionIOptionsSnapshot.cs +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/StartupInjectionOptionsSnapshot.cs @@ -7,11 +7,11 @@ namespace AspNetCore3 { - public class Startup_InjectionIOptionsSnapshot + public class StartupInjectionOptionsSnapshot { public IConfiguration Configuration { get; } - public Startup_InjectionIOptionsSnapshot(IConfiguration configuration) + public StartupInjectionOptionsSnapshot(IConfiguration configuration) { this.Configuration = configuration; } @@ -38,28 +38,27 @@ public void ConfigureServices(IServiceCollection services) { services.AddControllers(); - //`J IOptions - //services.AddOptions(); + //驗證 AppSetting services.AddOptions() .ValidateDataAnnotations() .Validate(p => { - if (p.AllowedHosts ==null) + if (p.AllowedHosts == null) { return false; } return true; }, "AllowedHosts must be value"); // Failure message. - ; - //`J IConfiguration + //注入 Options 和完整 IConfiguration + services.Configure(this.Configuration); + //注入 Options 和 Configuration Section Name services.Configure(this.Configuration); services.Configure("Player1", this.Configuration.GetSection("Player1")); services.Configure("Player2", this.Configuration.GetSection("Player2")); - - //services.AddSingleton(Configuration); + services.Configure("Player3", this.Configuration.GetSection("Player3")); } } } \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore3/appsettings.json b/Configuration/NetCore/Lab.Config/AspNetCore3/appsettings.json index 39d2dbef..c03edfe4 100644 --- a/Configuration/NetCore/Lab.Config/AspNetCore3/appsettings.json +++ b/Configuration/NetCore/Lab.Config/AspNetCore3/appsettings.json @@ -6,22 +6,25 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "", - + "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnectionString": "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;" + "DefaultConnectionString": "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;", + "AuthenticationConnectionString": "" }, "Player": { - "AppId": "testApp", - "Key": "12345678990" + "AppId": "player", + "Key": "1234567890" }, - "Player1": { - "AppId": "testApp", + "AppId": "player1", "Key": "12345678990" }, "Player2": { - "AppId": "testApp", - "Key": "12345678990" + "AppId": "player2", + "Key": "player2_123456" + }, + "Player3": { + "AppId": "player3", + "Key": "player3_123456" } -} +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore5/AspNetCore5.csproj b/Configuration/NetCore/Lab.Config/AspNetCore5/AspNetCore5.csproj new file mode 100644 index 00000000..92063f64 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/AspNetCore5/AspNetCore5.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + bin + bin\AspNetCore5.xml + + + + + + + + + + + + + diff --git a/Configuration/NetCore/Lab.Config/AspNetCore5/Controllers/DefaultController.cs b/Configuration/NetCore/Lab.Config/AspNetCore5/Controllers/DefaultController.cs new file mode 100644 index 00000000..9e817d64 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/AspNetCore5/Controllers/DefaultController.cs @@ -0,0 +1,68 @@ +using System; +using Lab.Infra; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace AspNetCore5.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DefaultController : ControllerBase + { + [HttpGet] + [Route("options/appsettings")] + public IActionResult Get() + { + var serviceProvider = this.HttpContext.RequestServices; + var content = serviceProvider.GetService>()?.Value; + return this.Ok(content); + } + + [HttpGet] + [Route("config/appsettings")] + public IActionResult GetConfig() + { + var serviceProvider = this.HttpContext.RequestServices; + var config = serviceProvider.GetService(); + var content = new AppSetting(); + config.Bind(content); + return this.Ok(content); + } + + [HttpGet] + [Route("monitor/players/{id}")] + public IActionResult GetMonitorPlayer(int id) + { + var serviceProvider = this.HttpContext.RequestServices; + var appSettingOptions = serviceProvider.GetService>(); + var playerOptions = serviceProvider.GetService>(); + var content = new + { + App = appSettingOptions?.CurrentValue, + Player = playerOptions?.Get($"Player{id}") + }; + appSettingOptions.OnChange(p => + { + Console.WriteLine("節點已變更"); + }); + return this.Ok(content); + } + + [HttpGet] + [Route("snapshot/players/{id}")] + public IActionResult GetSnapshotPlayer(int id) + { + var serviceProvider = this.HttpContext.RequestServices; + var appSettingOptions = serviceProvider.GetService>(); + var playerOptions = serviceProvider.GetService>(); + var content = new + { + App = appSettingOptions?.Value, + Player = playerOptions?.Get($"Player{id}") + }; + return this.Ok(content); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore5/Controllers/WeatherForecastController.cs b/Configuration/NetCore/Lab.Config/AspNetCore5/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..dab91d80 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/AspNetCore5/Controllers/WeatherForecastController.cs @@ -0,0 +1,39 @@ +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading.Tasks; +// using Microsoft.AspNetCore.Mvc; +// using Microsoft.Extensions.Logging; +// +// namespace AspNetCore5.Controllers +// { +// [ApiController] +// [Route("[controller]")] +// public class WeatherForecastController : ControllerBase +// { +// private static readonly string[] Summaries = new[] +// { +// "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +// }; +// +// private readonly ILogger _logger; +// +// public WeatherForecastController(ILogger logger) +// { +// _logger = logger; +// } +// +// [HttpGet] +// public IEnumerable Get() +// { +// var rng = new Random(); +// return Enumerable.Range(1, 5).Select(index => new WeatherForecast +// { +// Date = DateTime.Now.AddDays(index), +// TemperatureC = rng.Next(-20, 55), +// Summary = Summaries[rng.Next(Summaries.Length)] +// }) +// .ToArray(); +// } +// } +// } \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore5/Program.cs b/Configuration/NetCore/Lab.Config/AspNetCore5/Program.cs new file mode 100644 index 00000000..1fc668cf --- /dev/null +++ b/Configuration/NetCore/Lab.Config/AspNetCore5/Program.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace AspNetCore5 +{ + public class Program + { + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureAppConfiguration(p => + { + // 不重新載入組態 + //p.AddJsonFile("appsettings.json", false, false); + }); + webBuilder.UseStartup(); + }); + } + + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore5/Properties/launchSettings.json b/Configuration/NetCore/Lab.Config/AspNetCore5/Properties/launchSettings.json new file mode 100644 index 00000000..b38cbf0c --- /dev/null +++ b/Configuration/NetCore/Lab.Config/AspNetCore5/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:32162", + "sslPort": 44347 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "AspNetCore5": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Configuration/NetCore/Lab.Config/AspNetCore5/Startup.cs b/Configuration/NetCore/Lab.Config/AspNetCore5/Startup.cs new file mode 100644 index 00000000..78d69940 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/AspNetCore5/Startup.cs @@ -0,0 +1,79 @@ +using Lab.Infra; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; + +namespace AspNetCore5 +{ + public class Startup + { + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + { + this.Configuration = configuration; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "AspNetCore5 v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo {Title = "AspNetCore5", Version = "v1"}); + }); + + //驗證 AppSetting + services.AddOptions() + .ValidateDataAnnotations() + .Validate(p => + { + if (p.AllowedHosts == null) + { + return false; + } + + return true; + }, "AllowedHosts must be value"); // Failure message. + + //注入 Options 和完整 IConfiguration + services.Configure(this.Configuration); + + //注入 Options 和 Configuration Section Name + services.Configure("Player1", this.Configuration.GetSection("Player1")); + services.Configure("Player2", this.Configuration.GetSection("Player2")); + services.Configure("Player3", this.Configuration.GetSection("Player3")); + services.Configure("ConnectionStrings", this.Configuration.GetSection("ConnectionStrings")); + // services.PostConfigure("Player1", config => + // { + // config.AppId = "post_configured_option1_value"; + // }); + // services.PostConfigureAll(config => + // { + // config.Player.AppId = "post_configured_option1_value"; + // }); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore5/WeatherForecast.cs b/Configuration/NetCore/Lab.Config/AspNetCore5/WeatherForecast.cs new file mode 100644 index 00000000..393f1d76 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/AspNetCore5/WeatherForecast.cs @@ -0,0 +1,15 @@ +using System; + +namespace AspNetCore5 +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int) (TemperatureC / 0.5556); + + public string Summary { get; set; } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/AspNetCore5/appsettings.Development.json b/Configuration/NetCore/Lab.Config/AspNetCore5/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/Configuration/NetCore/Lab.Config/AspNetCore5/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Configuration/NetCore/Lab.Config/AspNetCore5/appsettings.json b/Configuration/NetCore/Lab.Config/AspNetCore5/appsettings.json new file mode 100644 index 00000000..25a5a48f --- /dev/null +++ b/Configuration/NetCore/Lab.Config/AspNetCore5/appsettings.json @@ -0,0 +1,30 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnectionString": "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;", + "AuthenticationConnectionString": "" + }, + "Player": { + "AppId": "player23", + "Key": "1234567890" + }, + "Player1": { + "AppId": "player1", + "Key": "12345678990" + }, + "Player2": { + "AppId": "player2", + "Key": "player2_123456" + }, + "Player3": { + "AppId": "player3", + "Key": "player3_123456" + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/Lab.Config.sln b/Configuration/NetCore/Lab.Config/Lab.Config.sln index d3dc3987..11296a46 100644 --- a/Configuration/NetCore/Lab.Config/Lab.Config.sln +++ b/Configuration/NetCore/Lab.Config/Lab.Config.sln @@ -9,24 +9,47 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MsUnitTest", "MsUnitTest\Ms EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore3", "AspNetCore3\AspNetCore3.csproj", "{2362CB3D-B69D-4C2D-B873-685F81140D1B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetFx48", "NetFx48\NetFx48.csproj", "{D0B23D3B-F2E0-4B38-8864-F3FF60EE115B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore5", "AspNetCore5\AspNetCore5.csproj", "{5D04A967-5E69-4CD7-AE41-F8BAD30D7DC2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + QA|Any CPU = QA|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {438294F7-7612-4190-A769-EFA5F34118DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {438294F7-7612-4190-A769-EFA5F34118DD}.Debug|Any CPU.Build.0 = Debug|Any CPU {438294F7-7612-4190-A769-EFA5F34118DD}.Release|Any CPU.ActiveCfg = Release|Any CPU {438294F7-7612-4190-A769-EFA5F34118DD}.Release|Any CPU.Build.0 = Release|Any CPU + {438294F7-7612-4190-A769-EFA5F34118DD}.QA|Any CPU.ActiveCfg = QA|Any CPU + {438294F7-7612-4190-A769-EFA5F34118DD}.QA|Any CPU.Build.0 = QA|Any CPU {623F3B3A-4C40-40C7-94C1-DBD7CD928CC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {623F3B3A-4C40-40C7-94C1-DBD7CD928CC4}.Debug|Any CPU.Build.0 = Debug|Any CPU {623F3B3A-4C40-40C7-94C1-DBD7CD928CC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {623F3B3A-4C40-40C7-94C1-DBD7CD928CC4}.Release|Any CPU.Build.0 = Release|Any CPU + {623F3B3A-4C40-40C7-94C1-DBD7CD928CC4}.QA|Any CPU.ActiveCfg = QA|Any CPU + {623F3B3A-4C40-40C7-94C1-DBD7CD928CC4}.QA|Any CPU.Build.0 = QA|Any CPU {2362CB3D-B69D-4C2D-B873-685F81140D1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2362CB3D-B69D-4C2D-B873-685F81140D1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {2362CB3D-B69D-4C2D-B873-685F81140D1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {2362CB3D-B69D-4C2D-B873-685F81140D1B}.Release|Any CPU.Build.0 = Release|Any CPU + {2362CB3D-B69D-4C2D-B873-685F81140D1B}.QA|Any CPU.ActiveCfg = QA|Any CPU + {2362CB3D-B69D-4C2D-B873-685F81140D1B}.QA|Any CPU.Build.0 = QA|Any CPU + {D0B23D3B-F2E0-4B38-8864-F3FF60EE115B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0B23D3B-F2E0-4B38-8864-F3FF60EE115B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0B23D3B-F2E0-4B38-8864-F3FF60EE115B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0B23D3B-F2E0-4B38-8864-F3FF60EE115B}.Release|Any CPU.Build.0 = Release|Any CPU + {D0B23D3B-F2E0-4B38-8864-F3FF60EE115B}.QA|Any CPU.ActiveCfg = QA|Any CPU + {D0B23D3B-F2E0-4B38-8864-F3FF60EE115B}.QA|Any CPU.Build.0 = QA|Any CPU + {5D04A967-5E69-4CD7-AE41-F8BAD30D7DC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D04A967-5E69-4CD7-AE41-F8BAD30D7DC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D04A967-5E69-4CD7-AE41-F8BAD30D7DC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D04A967-5E69-4CD7-AE41-F8BAD30D7DC2}.Release|Any CPU.Build.0 = Release|Any CPU + {5D04A967-5E69-4CD7-AE41-F8BAD30D7DC2}.QA|Any CPU.ActiveCfg = Debug|Any CPU + {5D04A967-5E69-4CD7-AE41-F8BAD30D7DC2}.QA|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Configuration/NetCore/Lab.Config/Lab.Infra/ConnectionStrings.cs b/Configuration/NetCore/Lab.Config/Lab.Infra/ConnectionStrings.cs index 45b7fe3e..bd90c3ca 100644 --- a/Configuration/NetCore/Lab.Config/Lab.Infra/ConnectionStrings.cs +++ b/Configuration/NetCore/Lab.Config/Lab.Infra/ConnectionStrings.cs @@ -1,4 +1,4 @@ -namespace Lab.Infra +namespace Lab.Infra { public class ConnectionStrings { diff --git a/Configuration/NetCore/Lab.Config/Lab.Infra/Lab.Infra.csproj b/Configuration/NetCore/Lab.Config/Lab.Infra/Lab.Infra.csproj index 792466b1..fc35969d 100644 --- a/Configuration/NetCore/Lab.Config/Lab.Infra/Lab.Infra.csproj +++ b/Configuration/NetCore/Lab.Config/Lab.Infra/Lab.Infra.csproj @@ -1,11 +1,18 @@ - - netcoreapp3.1 - + + netcoreapp3.1 + Debug;Release;QA + AnyCPU + - - - + + true + false + + + + + diff --git a/Configuration/NetCore/Lab.Config/Lab.Infra/Player.cs b/Configuration/NetCore/Lab.Config/Lab.Infra/Player.cs index 9c2a26ea..2ef82d7a 100644 --- a/Configuration/NetCore/Lab.Config/Lab.Infra/Player.cs +++ b/Configuration/NetCore/Lab.Config/Lab.Infra/Player.cs @@ -1,4 +1,4 @@ -namespace Lab.Infra +namespace Lab.Infra { public class Player { diff --git a/Configuration/NetCore/Lab.Config/MsUnitTest/MsUnitTest.csproj b/Configuration/NetCore/Lab.Config/MsUnitTest/MsUnitTest.csproj index a32946c2..e3c2748e 100644 --- a/Configuration/NetCore/Lab.Config/MsUnitTest/MsUnitTest.csproj +++ b/Configuration/NetCore/Lab.Config/MsUnitTest/MsUnitTest.csproj @@ -1,28 +1,37 @@ - - netcoreapp3.1 - - false - - - - - - - - - - - - - - - - - - Always - - + + netcoreapp3.1 + + false + + Debug;Release;QA + + AnyCPU + + + + true + false + + + + + + + + + + + + + + + + + + Always + + diff --git a/Configuration/NetCore/Lab.Config/MsUnitTest/UnitTest1.cs b/Configuration/NetCore/Lab.Config/MsUnitTest/UnitTest1.cs index fc3a8923..42df9474 100644 --- a/Configuration/NetCore/Lab.Config/MsUnitTest/UnitTest1.cs +++ b/Configuration/NetCore/Lab.Config/MsUnitTest/UnitTest1.cs @@ -10,7 +10,7 @@ namespace MsUnitTest public class UnitTest1 { [TestMethod] - public void zLAppSettingŪ]w() + public void 透過AppSetting物件讀取設定檔() { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) @@ -24,7 +24,7 @@ public class UnitTest1 } [TestMethod] - public void zLAppSettingŪ]w_ϬqsbߥXҥ~() + public void 透過AppSetting物件讀取設定檔_區段不存在拋出例外() { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) @@ -38,7 +38,7 @@ public class UnitTest1 } [TestMethod] - public void jw]w_XRk_Get() + public void 綁定設定_擴充方法_Get() { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) @@ -50,7 +50,7 @@ public class UnitTest1 } [TestMethod] - public void jw]w_XRk_Bind() + public void 綁定設定_擴充方法_Bind() { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) @@ -64,7 +64,7 @@ public class UnitTest1 } [TestMethod] - public void Ū]w() + public void 讀取設定檔() { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) @@ -78,7 +78,7 @@ public class UnitTest1 } [TestMethod] - public void Ū]w_GetConnectionString() + public void 讀取設定檔_GetConnectionString() { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) @@ -92,7 +92,7 @@ public class UnitTest1 } [TestMethod] - public void Ū]w_TryGet() + public void 讀取設定檔_TryGet() { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) diff --git a/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlow.cs b/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlow.cs new file mode 100644 index 00000000..85244965 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlow.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Configuration; + +namespace NetFx48 +{ + public class AppWorkFlow : IAppWorkFlow + { + private readonly IConfiguration _config; + + public AppWorkFlow(IConfiguration config) + { + this._config = config; + } + + public string GetPlayerId() + { + return this._config.GetSection("Player:AppId").Value; + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlow1.cs b/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlow1.cs new file mode 100644 index 00000000..dfaf1482 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlow1.cs @@ -0,0 +1,16 @@ +namespace NetFx48 +{ + public class AppWorkFlow1 : IAppWorkFlow + { + private AppSetting _appSetting; + + public AppWorkFlow1(AppSetting appSetting) + { + this._appSetting = appSetting; + } + public string GetPlayerId() + { + return this._appSetting.Player.AppId; + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlowWithOption.cs b/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlowWithOption.cs new file mode 100644 index 00000000..8fa36c8b --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlowWithOption.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.Extensions.Options; + +namespace NetFx48 +{ + public class AppWorkFlowWithOption : IAppWorkFlow + { + private readonly AppSetting1 _appSetting; + + public AppWorkFlowWithOption(IOptions options) + { + try + { + this._appSetting = options.Value; + } + catch (OptionsValidationException ex) + { + foreach (var failure in ex.Failures) + { + Console.WriteLine(failure); + } + } + } + + public string GetPlayerId() + { + return this._appSetting.Player.AppId; + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlowWithOptionsMonitor.cs b/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlowWithOptionsMonitor.cs new file mode 100644 index 00000000..97fe82cb --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlowWithOptionsMonitor.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Extensions.Options; + +namespace NetFx48 +{ + public class AppWorkFlowWithOptionsMonitor : IAppWorkFlow + { + private readonly AppSetting1 _appSetting; + private readonly Player1 _player; + + public AppWorkFlowWithOptionsMonitor(IOptionsMonitor appSettingOption, + IOptionsMonitor playerOption) + { + this._player = playerOption.Get("Player"); + this._appSetting = appSettingOption?.CurrentValue; + + Console.WriteLine($"AppSetting.Player.AppId = {this._appSetting.Player.AppId}"); + Console.WriteLine($"Player.AppId = {this._player.AppId}"); + } + + public string GetPlayerId() + { + return this._player.AppId; + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlowWithOptionsSnapshot.cs b/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlowWithOptionsSnapshot.cs new file mode 100644 index 00000000..72a1f7c6 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/AppWorkFlowWithOptionsSnapshot.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Extensions.Options; + +namespace NetFx48 +{ + public class AppWorkFlowWithOptionsSnapshot : IAppWorkFlow + { + private readonly AppSetting1 _appSetting; + private readonly Player1 _player; + + public AppWorkFlowWithOptionsSnapshot(IOptionsSnapshot appSettingOption, + IOptionsSnapshot playerOption) + { + this._player = playerOption?.Value; + this._appSetting = appSettingOption?.Value; + + Console.WriteLine($"AppSetting.Player.AppId = {this._appSetting.Player.AppId}"); + Console.WriteLine($"Player.AppId = {this._player.AppId}"); + } + + public string GetPlayerId() + { + return this._player.AppId; + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/IAppWorkFlow.cs b/Configuration/NetCore/Lab.Config/NetFx48/IAppWorkFlow.cs new file mode 100644 index 00000000..3c7de088 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/IAppWorkFlow.cs @@ -0,0 +1,7 @@ +namespace NetFx48 +{ + public interface IAppWorkFlow + { + string GetPlayerId(); + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/Models/AppSetting.cs b/Configuration/NetCore/Lab.Config/NetFx48/Models/AppSetting.cs new file mode 100644 index 00000000..21ad3ba8 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/Models/AppSetting.cs @@ -0,0 +1,17 @@ +namespace NetFx48 +{ + public struct AppSetting + { + public Player Player { get; set; } + + public ConnectionStrings ConnectionStrings { get; set; } + } + + public class AppSetting1 + { + public Player Player { get; set; } + + public ConnectionStrings ConnectionStrings { get; set; } + } + +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/Models/ConnectionStrings.cs b/Configuration/NetCore/Lab.Config/NetFx48/Models/ConnectionStrings.cs new file mode 100644 index 00000000..b4baf941 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/Models/ConnectionStrings.cs @@ -0,0 +1,9 @@ +namespace NetFx48 +{ + public struct ConnectionStrings + { + public string DefaultConnectionString { get; set; } + + public string AuthenticationConnectionString { get; set; } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/Models/Player.cs b/Configuration/NetCore/Lab.Config/NetFx48/Models/Player.cs new file mode 100644 index 00000000..7b778d7d --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/Models/Player.cs @@ -0,0 +1,16 @@ +namespace NetFx48 +{ + public struct Player + { + public string AppId { get; set; } + + public string Key { get; set; } + } + + public class Player1 + { + public string AppId { get; set; } + + public string Key { get; set; } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/NetFx48.csproj b/Configuration/NetCore/Lab.Config/NetFx48/NetFx48.csproj new file mode 100644 index 00000000..92ceb1d4 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/NetFx48.csproj @@ -0,0 +1,65 @@ + + + + net48 + + false + + latest + + Debug;Release;QA + + AnyCPU + 659be13b-676e-4c9e-a0b9-0df2ffd75cfc + + + + true + false + + + + + + + + + + + + + + + + + + + + + true + Always + PreserveNewest + + + true + Always + PreserveNewest + + + + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/Configuration/NetCore/Lab.Config/NetFx48/Properties/launchSettings.json b/Configuration/NetCore/Lab.Config/NetFx48/Properties/launchSettings.json new file mode 100644 index 00000000..e4d12871 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "NetFx48": { + "commandName": "Project", + "commandLineArgs": "--AppId=1234567890" + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/SurveyCommandConfigurationTests.cs b/Configuration/NetCore/Lab.Config/NetFx48/SurveyCommandConfigurationTests.cs new file mode 100644 index 00000000..6e1eaddb --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/SurveyCommandConfigurationTests.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.CommandLine; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NetFx48 +{ + [TestClass] + public class SurveyCommandConfigurationTests + { + [TestMethod] + public void 命令對應() + { + string[] args = {"-i=1234567890", "-c=app.json"}; + + var map = new Dictionary + { + {"-i", "AppId"}, + {"-c", "Config"} + }; + + var provider = new CommandLineConfigurationProvider(args, map); + provider.Load(); + + provider.TryGet("AppId", out var appId); + provider.TryGet("Config", out var configPath); + Console.WriteLine($"{args.First()}\r\n" + + $"AppId:{appId}\r\n" + + $"ConfigPath:{configPath}"); + } + + [TestMethod] + [DataRow(new[] {"-i=1234567890", "-c=app.json"})] + public void 命令對應_Host(string[] args) + { + var map = new Dictionary + { + {"-i", "AppId"}, + {"-c", "Config"} + }; + var builder = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration(config => + { + // config.Sources.Clear(); + config.AddCommandLine(args, map); + var configRoot = config.Build(); + + var appId = configRoot["AppId"]; + var configPath = configRoot["Config"]; + Console.WriteLine($"{args.First()}\r\n" + + $"AppId:{appId}\r\n" + + $"ConfigPath:{configPath}"); + }) + .ConfigureServices(service => + { + //DI + service.AddScoped(typeof(AppWorkFlow)); + }) + ; + var host = builder.Build(); + } + + [TestMethod] + [DataRow(new[] {"--AppId=1234567890"})] + [DataRow(new[] {"/AppId=1234567890"})] + [DataRow(new[] {"AppId=1234567890"})] + public void 實例化CommandLineConfigurationProvider(string[] args) + { + var provider = new CommandLineConfigurationProvider(args); + provider.Load(); + provider.TryGet("AppId", out var appId); + Console.WriteLine($"{args.First()}\r\n" + + $"AppId:{appId}"); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/SurveyEnvironmentVariablesConfigurationTests.cs b/Configuration/NetCore/Lab.Config/NetFx48/SurveyEnvironmentVariablesConfigurationTests.cs new file mode 100644 index 00000000..aed350b8 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/SurveyEnvironmentVariablesConfigurationTests.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NetFx48 +{ + [TestClass] + public class SurveyEnvironmentVariablesConfigurationTests + { + [TestMethod] + public void Host實例化ConfigurationBuilder() + { + var builder = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((hosting, configBuilder) => + { + // config.Sources.Clear(); + var hostingEnvironmentEnvironmentName = + hosting.HostingEnvironment.EnvironmentName; + configBuilder.AddEnvironmentVariables("Custom_"); + var configRoot = configBuilder.Build(); + + //讀取組態 + Console + .WriteLine($"ASPNETCORE_ENVIRONMENT = {configRoot["ASPNETCORE_ENVIRONMENT"]}"); + Console + .WriteLine($"DOTNET_ENVIRONMENT = {configRoot["DOTNET_ENVIRONMENT"]}"); + Console + .WriteLine($"CUSTOM_ENVIRONMENT = {configRoot["CUSTOM_ENVIRONMENT"]}"); + Console + .WriteLine($"ENVIRONMENT1 = {configRoot["ENVIRONMENT1"]}"); + }) + ; + var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + Console.WriteLine($"EnvironmentName={environment.EnvironmentName}"); + } + + [TestMethod] + public void 切換組態設定() + { + var builder = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((hosting, configBuilder) => + { + // config.Sources.Clear(); + var environmentName = + hosting.Configuration["ENVIRONMENT2"]; + configBuilder.AddJsonFile("appsettings.json", false, true); + configBuilder + .AddJsonFile($"appsettings.{environmentName}.json", + true, true); + + var configRoot = configBuilder.Build(); + + //讀取組態 + Console.WriteLine($"AppId = {configRoot["Player:AppId"]}"); + Console.WriteLine($"Key = {configRoot["Player:Key"]}"); + Console + .WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}"); + }) + ; + builder.Build(); + } + + [TestMethod] + public void 手動實例化ConfigurationBuilder() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddEnvironmentVariables("ASPNETCORE_") + ; + + var configRoot = configBuilder.Build(); + + //讀取組態 + Console.WriteLine($"ENVIRONMENT = {configRoot["ENVIRONMENT"]}"); + } + + [TestMethod] + public void 設定主機組態() + { + var builder = Host.CreateDefaultBuilder() + .ConfigureHostConfiguration(config => + { + config.AddJsonFile("appsettings.json", false, true); + }) + ; + + var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + Console.WriteLine($"EnvironmentName={environment.EnvironmentName}"); + } + + [TestMethod] + public void 讀取環境變數() + { + Environment.SetEnvironmentVariable("Player:AppId", "player1"); + Environment.SetEnvironmentVariable("Player:Key", "1234567890"); + Environment.SetEnvironmentVariable("ConnectionStrings:DefaultConnectionString", + "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"); + var configBuilder = new ConfigurationBuilder().AddEnvironmentVariables(); + + var configRoot = configBuilder.Build(); + + //讀取組態 + + Console.WriteLine($"AppId = {configRoot["AppId"]}"); + Console.WriteLine($"AppId = {configRoot["Player:AppId"]}"); + Console.WriteLine($"Key = {configRoot["Player:Key"]}"); + Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}"); + } + + + [TestMethod] + public void 讀取環境變數_綁定() + { + Environment.SetEnvironmentVariable("Player:AppId", "player1"); + Environment.SetEnvironmentVariable("Player:Key", "1234567890"); + Environment.SetEnvironmentVariable("ConnectionStrings:DefaultConnectionString", + "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"); + var configBuilder = new ConfigurationBuilder().AddEnvironmentVariables(); + + var configRoot = configBuilder.Build(); + var appSetting = configRoot.Get(); + + //讀取組態 + + Console.WriteLine($"AppId = {appSetting.Player.AppId}"); + Console.WriteLine($"Key = {appSetting.Player.Key}"); + Console.WriteLine($"Connection String = {appSetting.ConnectionStrings.DefaultConnectionString}"); + } + + [TestMethod] + public void 讀取環境變數_綁定_集合() + { + Environment.SetEnvironmentVariable("a:Player:AppId", "player1"); + Environment.SetEnvironmentVariable("a:Player:Key", "1234567890"); + Environment.SetEnvironmentVariable("a:ConnectionStrings:DefaultConnectionString", + "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"); + Environment.SetEnvironmentVariable("b:Player:AppId", "player2"); + Environment.SetEnvironmentVariable("b:Player:Key", "1234567890"); + Environment.SetEnvironmentVariable("b:ConnectionStrings:DefaultConnectionString", + "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"); + + var configBuilder = new ConfigurationBuilder().AddEnvironmentVariables(); + + var configRoot = configBuilder.Build(); + var appSettings = configRoot.Get>(); + + //讀取組態 + + Console.WriteLine($"AppId = {appSettings[0].Player.AppId}"); + Console.WriteLine($"Key = {appSettings[0].Player.Key}"); + Console.WriteLine($"Connection String = {appSettings[0].ConnectionStrings.DefaultConnectionString}"); + Console.WriteLine($"AppId = {appSettings[1].Player.AppId}"); + Console.WriteLine($"Key = {appSettings[1].Player.Key}"); + Console.WriteLine($"Connection String = {appSettings[1].ConnectionStrings.DefaultConnectionString}"); + } + + [TestMethod] + public void 讀取環境變數_綁定_字典() + { + Environment.SetEnvironmentVariable("a:Player:AppId", "player1"); + Environment.SetEnvironmentVariable("a:Player:Key", "1234567890"); + Environment.SetEnvironmentVariable("a:ConnectionStrings:DefaultConnectionString", + "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"); + Environment.SetEnvironmentVariable("b:Player:AppId", "player2"); + Environment.SetEnvironmentVariable("b:Player:Key", "1234567890"); + Environment.SetEnvironmentVariable("b:ConnectionStrings:DefaultConnectionString", + "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"); + + var configBuilder = new ConfigurationBuilder().AddEnvironmentVariables(); + + var configRoot = configBuilder.Build(); + var appSettings = configRoot.Get>(); + + //讀取組態 + + Console.WriteLine($"AppId = {appSettings["a"].Player.AppId}"); + Console.WriteLine($"Key = {appSettings["a"].Player.Key}"); + Console.WriteLine($"Connection String = {appSettings["a"].ConnectionStrings.DefaultConnectionString}"); + Console.WriteLine($"AppId = {appSettings["b"].Player.AppId}"); + Console.WriteLine($"Key = {appSettings["b"].Player.Key}"); + Console.WriteLine($"Connection String = {appSettings["b"].ConnectionStrings.DefaultConnectionString}"); + } + + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/SurveyIniConfigurationTests.cs b/Configuration/NetCore/Lab.Config/NetFx48/SurveyIniConfigurationTests.cs new file mode 100644 index 00000000..aa2f3c96 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/SurveyIniConfigurationTests.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NetFx48 +{ + [TestClass] + public class SurveyIniConfigurationTests + { + [TestMethod] + public void 手動實例化ConfigurationBuilder() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddIniFile("appsettings.ini", optional: false, reloadOnChange: true); + var configRoot = configBuilder.Build(); + + //讀取組態 + Console.WriteLine($"AppId = {configRoot["AppId"]}"); + Console.WriteLine($"AppId = {configRoot["Player:AppId"]}"); + Console.WriteLine($"Key = {configRoot["Player:Key"]}"); + Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}"); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/SurveyJsonConfigurationTests.cs b/Configuration/NetCore/Lab.Config/NetFx48/SurveyJsonConfigurationTests.cs new file mode 100644 index 00000000..8118d15a --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/SurveyJsonConfigurationTests.cs @@ -0,0 +1,198 @@ +using System; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NetFx48 +{ + [TestClass] + public class SurveyJsonConfigurationTests + { + [TestMethod] + public void 切換組態() + { + string environmentName; +#if DEBUG + environmentName = "Development"; +#elif QA + environmentName = "QA"; +#elif STAGING + environmentName = "Staging"; +#elif RELEASE + environmentName = "Production"; +#endif + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", false, true) + .AddJsonFile($"appsettings.{environmentName}.json", true, true) + ; + var configRoot = configBuilder.Build(); + + //讀取組態 + Console.WriteLine($"AppId = {configRoot["Player:AppId"]}"); + Console.WriteLine($"Key = {configRoot["Player:Key"]}"); + Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}"); + } + + [TestMethod] + public void 手動實例化ConfigurationBuilder() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, true) + ; + var configRoot = configBuilder.Build(); + + //讀取組態 + + Console.WriteLine($"AppId = {configRoot["AppId"]}"); + Console.WriteLine($"AppId = {configRoot["Player:AppId"]}"); + Console.WriteLine($"Key = {configRoot["Player:Key"]}"); + Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}"); + } + + [TestMethod] + public void 注入Configuration() + { + var builder = Host.CreateDefaultBuilder(null) + .ConfigureAppConfiguration(config => + { + config.Sources.Clear(); + config.AddJsonFile("appsettings.json", true, true); + }) + .ConfigureServices(service => + { + //DI + service.AddScoped(typeof(AppWorkFlow)); + }); + var host = builder.Build(); + + var appService = host.Services.GetService(); + var playerId = appService.GetPlayerId(); + Console.WriteLine($"AppId = {playerId}"); + } + + [TestMethod] + public void 通過Host() + { + using var host = CreateHostBuilder(null).Build(); + } + + [TestMethod] + public void 實例化JsonConfigurationProvider() + { + var configProvider = new JsonConfigurationProvider(new JsonConfigurationSource + { + Optional = false, + Path = "appsettings.json", + ReloadOnChange = true + }); + configProvider.Load(); + configProvider.TryGet("Player:AppId", out var appId); + Console.WriteLine($"AppId = {appId}"); + } + + [TestMethod] + public void 讀取設定檔_GetChild() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + var configRoot = builder.Build(); + var firstSections = configRoot.GetChildren(); + foreach (var firstSection in firstSections) + { + var secondSections = firstSection.GetChildren(); + foreach (var secondSection in secondSections) + { + Console.WriteLine($"{secondSection.Key}={secondSection.Value}\tPath={secondSection.Path}"); + } + } + } + + [TestMethod] + public void 讀取設定檔_GetSection() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + var configRoot = builder.Build(); + + Console.WriteLine($"AppId = {configRoot.GetSection("AppId")}"); + Console.WriteLine($"AppId = {configRoot.GetSection("Player:AppId")}"); + Console.WriteLine($"Key = {configRoot.GetSection("Player:Key")}"); + Console.WriteLine($"Connection String = {configRoot.GetSection("ConnectionStrings:DefaultConnectionString")}"); + } + + [TestMethod] + public void 讀取設定檔_TryGet() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + var configRoot = builder.Build(); + + //TryGet + foreach (var provider in configRoot.Providers) + { + provider.TryGet("Player:AppId", out var value); + Console.WriteLine($"AppId = {value}"); + } + } + + [TestMethod] + public void 讀取設定檔_綁定() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + var configRoot = builder.Build(); + + var appSetting = new AppSetting(); + configRoot.Bind(appSetting); + Console.WriteLine($"AppId = {appSetting.Player.AppId}"); + Console.WriteLine($"Key = {appSetting.Player.Key}"); + Console.WriteLine($"Connection String = {appSetting.ConnectionStrings.DefaultConnectionString}"); + } + + [TestMethod] + public void 讀取設定檔_綁定_Get() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + var configRoot = builder.Build(); + var player = configRoot.GetSection("Player").Get(); + var appSetting = configRoot.Get(); + + Console.WriteLine($"AppId = {player.AppId}"); + Console.WriteLine($"Key = {appSetting.Player.Key}"); + Console.WriteLine($"Connection String = {appSetting.ConnectionStrings.DefaultConnectionString}"); + } + + private static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(config => + { + config.Sources.Clear(); + config.AddJsonFile("appsettings.json", true, true); + var configRoot = config.Build(); + + //讀取組態 + Console.WriteLine($"AppId = {configRoot["AppId"]}"); + Console.WriteLine($"AppId = {configRoot["Player:AppId"]}"); + Console.WriteLine($"Key = {configRoot["Player:Key"]}"); + Console + .WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}"); + }) + .ConfigureServices(service => + { + //DI + }); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/SurveyKeyPerFileConfigurationTests.cs b/Configuration/NetCore/Lab.Config/NetFx48/SurveyKeyPerFileConfigurationTests.cs new file mode 100644 index 00000000..573309f1 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/SurveyKeyPerFileConfigurationTests.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NetFx48 +{ + [TestClass] + public class SurveyKeyPerFileConfigurationTests + { + [TestMethod] + public void 手動實例化ConfigurationBuilder() + { + var expected = "我是檔案內容"; + var folderPath = Path.Combine(Directory.GetCurrentDirectory(), "keys/aws/web"); + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddKeyPerFile(folderPath,false); + var configRoot = configBuilder.Build(); + + //讀取組態 + var actual = configRoot["NewFile1.txt"]; + Console.WriteLine($"NewFile1.txt = {actual}"); + Assert.AreEqual(expected,actual); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/SurveyMemoryConfigurationTests.cs b/Configuration/NetCore/Lab.Config/NetFx48/SurveyMemoryConfigurationTests.cs new file mode 100644 index 00000000..8aba7974 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/SurveyMemoryConfigurationTests.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NetFx48 +{ + [TestClass] + public class SurveyMemoryConfigurationTests + { + [TestMethod] + public void 讀取記憶體組態() + { + var configBuilder = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "Player:AppId", "player1" }, + { "Player:Key", "1234567890" }, + { + "ConnectionStrings:DefaultConnectionString", + "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;" + }, + }) + ; + + var configRoot = configBuilder.Build(); + + //讀取組態 + + Console.WriteLine($"AppId = {configRoot["AppId"]}"); + Console.WriteLine($"AppId = {configRoot["Player:AppId"]}"); + Console.WriteLine($"Key = {configRoot["Player:Key"]}"); + Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}"); + } + + [TestMethod] + public void 讀取記憶體組態_綁定() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddInMemoryCollection(new Dictionary + { + { "Player:AppId", "player1" }, + { "Player:Key", "1234567890" }, + { + "ConnectionStrings:DefaultConnectionString", + "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;" + }, + }) + ; + + var configRoot = configBuilder.Build(); + var appSetting = configRoot.Get(); + + //讀取組態 + + Console.WriteLine($"AppId = {appSetting.Player.AppId}"); + Console.WriteLine($"Key = {appSetting.Player.Key}"); + Console.WriteLine($"Connection String = {appSetting.ConnectionStrings.DefaultConnectionString}"); + } + + [TestMethod] + public void 讀取記憶體組態_綁定_集合() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddInMemoryCollection(new Dictionary + { + { "a:Player:AppId", "player1" }, + { "a:Player:Key", "1234567890" }, + { + "a:ConnectionStrings:DefaultConnectionString", + "a:Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;" + }, + { "b:Player:AppId", "player2" }, + { "b:Player:Key", "1234567890" }, + { + "b:ConnectionStrings:DefaultConnectionString", + "b:Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;" + }, + }) + ; + + var configRoot = configBuilder.Build(); + var appSettings = configRoot.Get>(); + + //讀取組態 + + Console.WriteLine($"AppId = {appSettings[0].Player.AppId}"); + Console.WriteLine($"Key = {appSettings[0].Player.Key}"); + Console.WriteLine($"Connection String = {appSettings[0].ConnectionStrings.DefaultConnectionString}"); + } + + [TestMethod] + public void 讀取記憶體組態_綁定_字典() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddInMemoryCollection(new Dictionary + { + { "a:Player:AppId", "player1" }, + { "a:Player:Key", "1234567890" }, + { + "a:ConnectionStrings:DefaultConnectionString", + "a:Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;" + }, + { "b:Player:AppId", "player2" }, + { "b:Player:Key", "1234567890" }, + { + "b:ConnectionStrings:DefaultConnectionString", + "b:Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;" + }, + }) + ; + + var configRoot = configBuilder.Build(); + var appSettings = configRoot.Get>(); + + //讀取組態 + + Console.WriteLine($"AppId = {appSettings["a"].Player.AppId}"); + Console.WriteLine($"Key = {appSettings["a"].Player.Key}"); + Console.WriteLine($"Connection String = {appSettings["a"].ConnectionStrings.DefaultConnectionString}"); + Console.WriteLine($"AppId = {appSettings["b"].Player.AppId}"); + Console.WriteLine($"Key = {appSettings["b"].Player.Key}"); + Console.WriteLine($"Connection String = {appSettings["b"].ConnectionStrings.DefaultConnectionString}"); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/SurveyOptionTests.cs b/Configuration/NetCore/Lab.Config/NetFx48/SurveyOptionTests.cs new file mode 100644 index 00000000..96824af4 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/SurveyOptionTests.cs @@ -0,0 +1,180 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NetFx48 +{ + [TestClass] + public class SurveyOptionTests + { + [TestMethod] + public void 注入Option() + { + var builder = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((hosting, configBuilder) => + { + // 1.讀組態檔 + var environmentName = + hosting.Configuration["ENVIRONMENT2"]; + configBuilder.AddJsonFile("appsettings.json", false, true); + configBuilder + .AddJsonFile($"appsettings.{environmentName}.json", + true, true); + }) + .ConfigureServices((hosting, services) => + { + // 2. 注入 Option 和 Configuration + services.Configure(hosting.Configuration); + + //注入其他服務 + services.AddSingleton(); + }) + ; + var host = builder.Build(); + var service = host.Services.GetService(); + var playerId = service.GetPlayerId(); + Console.WriteLine($"PlayerId = {playerId}"); + } + + [TestMethod] + public void 注入OptionMonitor() + { + var builder = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((hosting, configBuilder) => + { + // 1.讀組態檔 + var environmentName = + hosting.Configuration["ENVIRONMENT2"]; + configBuilder.AddJsonFile("appsettings.json", false, true); + configBuilder + .AddJsonFile($"appsettings.{environmentName}.json", + true, true); + }) + .ConfigureServices((hosting, services) => + { + // 注入 Option 和完整 Configuration + services.Configure(hosting.Configuration); + + // 注入 Option 和特定 Configuration Section Name + services.Configure("Player", + hosting.Configuration.GetSection("Player")); + + //注入其他服務 + services.AddScoped(); + }) + ; + var host = builder.Build(); + var service = host.Services.GetService(); + var playerId = service.GetPlayerId(); + Console.WriteLine($"PlayerId = {playerId}"); + } + + [TestMethod] + public void 注入OptionSnapshot() + { + var builder = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((hosting, configBuilder) => + { + var environmentName = + hosting.Configuration["ENVIRONMENT2"]; + configBuilder.AddJsonFile("appsettings.json", false, true); + configBuilder + .AddJsonFile($"appsettings.{environmentName}.json", + true, true); + }) + .ConfigureServices((hosting, services) => + { + // 注入 Option by 完整組態 + services.Configure(hosting.Configuration); + + // 注入 Option by 特定組態 + services.Configure(hosting.Configuration + .GetSection("Player")); + + //注入其他服務 + services.AddScoped(); + }) + ; + var host = builder.Build(); + var service = host.Services.GetService(); + var playerId = service.GetPlayerId(); + Console.WriteLine($"PlayerId = {playerId}"); + } + +[TestMethod] + public void 驗證() + { + var builder = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((hosting, configBuilder) => + { + // 1.讀組態檔 + var environmentName = + hosting.Configuration["ENVIRONMENT2"]; + configBuilder.AddJsonFile("appsettings.json", false, true); + configBuilder + .AddJsonFile($"appsettings.{environmentName}.json", + true, true); + }) + .ConfigureServices((hosting, services) => + { + // 2. 注入 Option 和 Configuration + services.Configure(hosting.Configuration); + //驗證 + services.AddOptions() + .ValidateDataAnnotations() + .Validate(p => + { + var hasContent = string.IsNullOrWhiteSpace(p.ConnectionStrings.DefaultConnectionString); + if (hasContent == false) + { + return false; + } + + return true; + }, + "DefaultConnectionString must be value"); // Failure message. + ; + + //注入其他服務 + services.AddSingleton(); + }) + ; + var host = builder.Build(); + var service = host.Services.GetService(); + var playerId = service.GetPlayerId(); + Console.WriteLine($"PlayerId = {playerId}"); + } + + [TestMethod] + public void 直接注入組態物件() + { + var builder = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((hosting, configBuilder) => + { + // 1.讀組態檔 + var environmentName = + hosting.Configuration["ENVIRONMENT2"]; + configBuilder.AddJsonFile("appsettings.json", false, true); + configBuilder + .AddJsonFile($"appsettings.{environmentName}.json", + true, true); + }) + .ConfigureServices((hosting, services) => + { + var appSetting = hosting.Configuration.Get(); + services.AddSingleton(typeof(AppSetting), appSetting); + + //注入其他服務 + services.AddSingleton(); + }) + ; + var host = builder.Build(); + var service = host.Services.GetService(); + var playerId = service.GetPlayerId(); + Console.WriteLine($"PlayerId = {playerId}"); + } + + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/SurveyUserSecretTests.cs b/Configuration/NetCore/Lab.Config/NetFx48/SurveyUserSecretTests.cs new file mode 100644 index 00000000..32043141 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/SurveyUserSecretTests.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NetFx48 +{ + [TestClass] + public class SurveyUserSecretTests + { + [TestMethod] + public void Host讀取秘密() + { + var builder = Host.CreateDefaultBuilder() + .ConfigureHostConfiguration(config => + { + config.AddJsonFile("appsettings.json", false, true); + }) + ; + var host = builder.Build(); + + var config = host.Services.GetService(); + Console.WriteLine($"Player:Key = {config["Player:Key"]}"); + Console.WriteLine($"DbPassword = {config["DbPassword"]}"); + } + + [TestMethod] + public void 手動實例化組態讀取秘密() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .AddUserSecrets() + ; + + var config = builder.Build(); + Console.WriteLine($"Player:Key = {config["Player:Key"]}"); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/SurveyXmlConfigurationTests.cs b/Configuration/NetCore/Lab.Config/NetFx48/SurveyXmlConfigurationTests.cs new file mode 100644 index 00000000..15bf423b --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/SurveyXmlConfigurationTests.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NetFx48 +{ + [TestClass] + public class SurveyXmlConfigurationTests + { + [TestMethod] + public void 手動實例化ConfigurationBuilder() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddXmlFile("appsettings.xml", false, true); + var configRoot = configBuilder.Build(); + + //讀取組態 + Console.WriteLine($"AppId = {configRoot["AppId"]}"); + Console.WriteLine($"AppId = {configRoot["Player:AppId"]}"); + Console.WriteLine($"Key = {configRoot["Player:Key"]}"); + Console.WriteLine($"Connection String = {configRoot["ConnectionStrings:DefaultConnectionString"]}"); + } + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/appsettings.QA.json b/Configuration/NetCore/Lab.Config/NetFx48/appsettings.QA.json new file mode 100644 index 00000000..b12c223d --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/appsettings.QA.json @@ -0,0 +1,9 @@ +{ + "ConnectionStrings": { + "DefaultConnectionString": "Server=(localdb)\\mssqllocaldb;Database=ConsoleApp.NewDb.QA;Trusted_Connection=True;" + }, + "Player": { + "AppId": "qa", + "Key": "qa1234567890" + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/appsettings.ini b/Configuration/NetCore/Lab.Config/NetFx48/appsettings.ini new file mode 100644 index 00000000..2b9d12b9 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/appsettings.ini @@ -0,0 +1,6 @@ +[ConnectionStrings] +DefaultConnectionString = "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;" + +[Player] +AppId = testApp +Key = 12345678990 diff --git a/Configuration/NetCore/Lab.Config/NetFx48/appsettings.json b/Configuration/NetCore/Lab.Config/NetFx48/appsettings.json new file mode 100644 index 00000000..20f51da4 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/appsettings.json @@ -0,0 +1,11 @@ +{ + "ConnectionStrings": { + "DefaultConnectionString": "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;" + }, + "Player": { + "AppId": "player1", + "Key": "1234567890" + }, + "Environment": "Development", + "ApplicationName": "NetFx48" +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/appsettings.test.json b/Configuration/NetCore/Lab.Config/NetFx48/appsettings.test.json new file mode 100644 index 00000000..420fcb73 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/appsettings.test.json @@ -0,0 +1,9 @@ +{ + "ConnectionStrings": { + "DefaultConnectionString": "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb.Test;Trusted_Connection=True;" + }, + "Player": { + "AppId": "test", + "Key": "test1234567890" + } +} \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/appsettings.xml b/Configuration/NetCore/Lab.Config/NetFx48/appsettings.xml new file mode 100644 index 00000000..80f4425b --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/appsettings.xml @@ -0,0 +1,12 @@ + + + + + Server=(localdb)\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True; + + + + testApp + 12345678990 + + \ No newline at end of file diff --git a/Configuration/NetCore/Lab.Config/NetFx48/keys/aws/web/NewFile1.txt b/Configuration/NetCore/Lab.Config/NetFx48/keys/aws/web/NewFile1.txt new file mode 100644 index 00000000..8c7cec77 --- /dev/null +++ b/Configuration/NetCore/Lab.Config/NetFx48/keys/aws/web/NewFile1.txt @@ -0,0 +1 @@ +我是檔案內容 \ No newline at end of file diff --git a/Host/ConsoleAppNet5/ConsoleAppNet5.csproj b/Coravel/Lab.CoravelScheduler/ConsoleApp1/ConsoleApp1.csproj similarity index 100% rename from Host/ConsoleAppNet5/ConsoleAppNet5.csproj rename to Coravel/Lab.CoravelScheduler/ConsoleApp1/ConsoleApp1.csproj diff --git a/Host/ConsoleAppNet5/Program.cs b/Coravel/Lab.CoravelScheduler/ConsoleApp1/Program.cs similarity index 86% rename from Host/ConsoleAppNet5/Program.cs rename to Coravel/Lab.CoravelScheduler/ConsoleApp1/Program.cs index c6aa3ecd..be1b5acd 100644 --- a/Host/ConsoleAppNet5/Program.cs +++ b/Coravel/Lab.CoravelScheduler/ConsoleApp1/Program.cs @@ -1,6 +1,6 @@ using System; -namespace ConsoleAppNet5 +namespace ConsoleApp1 { class Program { diff --git a/Coravel/Lab.CoravelScheduler/Lab.CoravelScheduler.sln b/Coravel/Lab.CoravelScheduler/Lab.CoravelScheduler.sln new file mode 100644 index 00000000..cc4f87b3 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/Lab.CoravelScheduler.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30804.86 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.NetFx48", "WebApi.NetFx48\WebApi.NetFx48.csproj", "{F405417B-110F-4A6D-849E-AACDDE33F268}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.Net5", "WebApi.Net5\WebApi.Net5.csproj", "{F25A0C04-86A5-45F5-BE19-0DEF8F42E5AB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F405417B-110F-4A6D-849E-AACDDE33F268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F405417B-110F-4A6D-849E-AACDDE33F268}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F405417B-110F-4A6D-849E-AACDDE33F268}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F405417B-110F-4A6D-849E-AACDDE33F268}.Release|Any CPU.Build.0 = Release|Any CPU + {F25A0C04-86A5-45F5-BE19-0DEF8F42E5AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F25A0C04-86A5-45F5-BE19-0DEF8F42E5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F25A0C04-86A5-45F5-BE19-0DEF8F42E5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F25A0C04-86A5-45F5-BE19-0DEF8F42E5AB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {81604A6A-F5CD-44DF-AA86-039C10809520} + EndGlobalSection +EndGlobal diff --git a/Coravel/Lab.CoravelScheduler/WebApi.Net5/Controllers/WeatherForecastController.cs b/Coravel/Lab.CoravelScheduler/WebApi.Net5/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..7cd628f4 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.Net5/Controllers/WeatherForecastController.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace WebApi.Net5.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet] + public IEnumerable Get() + { + var rng = new Random(); + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateTime.Now.AddDays(index), + TemperatureC = rng.Next(-20, 55), + Summary = Summaries[rng.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.Net5/Program.cs b/Coravel/Lab.CoravelScheduler/WebApi.Net5/Program.cs new file mode 100644 index 00000000..b9a97cb6 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.Net5/Program.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace WebApi.Net5 +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.Net5/Properties/launchSettings.json b/Coravel/Lab.CoravelScheduler/WebApi.Net5/Properties/launchSettings.json new file mode 100644 index 00000000..286105a3 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.Net5/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:21317", + "sslPort": 44331 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WebApi.Net5": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Coravel/Lab.CoravelScheduler/WebApi.Net5/Startup.cs b/Coravel/Lab.CoravelScheduler/WebApi.Net5/Startup.cs new file mode 100644 index 00000000..34d7237e --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.Net5/Startup.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coravel; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; + +namespace WebApi.Net5 +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo {Title = "WebApi.Net5", Version = "v1"}); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApi.Net5 v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + + var provider = app.ApplicationServices; + provider.UseScheduler(scheduler => + { + scheduler.Schedule( + () => Console.WriteLine("Every minute during the week.") + ) + .EveryMinute() + .Weekday(); + }); + } + } +} \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.Net5/WeatherForecast.cs b/Coravel/Lab.CoravelScheduler/WebApi.Net5/WeatherForecast.cs new file mode 100644 index 00000000..e0b73592 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.Net5/WeatherForecast.cs @@ -0,0 +1,15 @@ +using System; + +namespace WebApi.Net5 +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int) (TemperatureC / 0.5556); + + public string Summary { get; set; } + } +} \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.Net5/WebApi.Net5.csproj b/Coravel/Lab.CoravelScheduler/WebApi.Net5/WebApi.Net5.csproj new file mode 100644 index 00000000..738a4465 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.Net5/WebApi.Net5.csproj @@ -0,0 +1,14 @@ + + + + net5.0 + + + + + + + + + + diff --git a/Coravel/Lab.CoravelScheduler/WebApi.Net5/appsettings.Development.json b/Coravel/Lab.CoravelScheduler/WebApi.Net5/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.Net5/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Coravel/Lab.CoravelScheduler/WebApi.Net5/appsettings.json b/Coravel/Lab.CoravelScheduler/WebApi.Net5/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.Net5/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/DefaultDependencyResolver.cs b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/DefaultDependencyResolver.cs new file mode 100644 index 00000000..4d95d01d --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/DefaultDependencyResolver.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Web.Http.Dependencies; +using Microsoft.Extensions.DependencyInjection; + +namespace WebApi.NetFx48 +{ + public class DefaultDependencyResolver : IDependencyResolver + { + protected IServiceProvider ServiceProvider { get; set; } + + public DefaultDependencyResolver(IServiceProvider serviceProvider) + { + this.ServiceProvider = serviceProvider; + } + + public object GetService(Type serviceType) + { + return this.ServiceProvider.GetService(serviceType); + } + + public IEnumerable GetServices(Type serviceType) + { + return this.ServiceProvider.GetServices(serviceType); + } + + public IDependencyScope BeginScope() + { + return new DefaultDependencyResolver(this.ServiceProvider.CreateScope().ServiceProvider); + } + + public void Dispose() + { + // you can implement this interface just when you use .net core 2.0 + // this.ServiceProvider.Dispose(); + } + } +} \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/DependencyInjectionConfig.cs b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/DependencyInjectionConfig.cs new file mode 100644 index 00000000..59b0f4a7 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/DependencyInjectionConfig.cs @@ -0,0 +1,32 @@ +using System.Web.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace WebApi.NetFx48 +{ + public class DependencyInjectionConfig + { + public static void Register(HttpConfiguration config) + { + var services = ConfigureServices(); + + var provider = services.BuildServiceProvider(); + + var resolver = new DefaultDependencyResolver(provider); + config.DependencyResolver = resolver; + } + + /// + /// 使用 MS DI 註冊 + /// + /// + private static ServiceCollection ConfigureServices() + { + var services = new ServiceCollection(); + + //使用 Microsoft.Extensions.DependencyInjection 註冊 + services.AddControllersAsServices(typeof(DependencyInjectionConfig).Assembly.GetExportedTypes()); + + return services; + } + } +} \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/ServiceProviderExtensions.cs b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/ServiceProviderExtensions.cs new file mode 100644 index 00000000..bba6d52d --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/ServiceProviderExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Http.Controllers; +using Microsoft.Extensions.DependencyInjection; + +namespace WebApi.NetFx48 +{ + public static class ServiceProviderExtensions + { + public static IServiceCollection AddControllersAsServices(this IServiceCollection services, + IEnumerable controllerTypes) + { + var filter = controllerTypes.Where(t => !t.IsAbstract + && !t.IsGenericTypeDefinition) + .Where(t => typeof(IHttpController).IsAssignableFrom(t) + || t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)); + + foreach (var type in filter) + { + services.AddTransient(type); + } + + return services; + } + } +} \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/SwaggerConfig.cs b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/SwaggerConfig.cs new file mode 100644 index 00000000..86dd47ab --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/SwaggerConfig.cs @@ -0,0 +1,374 @@ +using System; +using System.Linq; +using System.Web; +using System.Web.Http; +using System.Web.Http.Description; +using System.Web.Http.Routing.Constraints; +using System.Collections.Generic; + +using WebApi.NetFx48; +using Swagger.Net.Application; +using Swagger.Net; + +[assembly: PreApplicationStartMethod(typeof(SwaggerConfig), "Register")] + +namespace WebApi.NetFx48 +{ + public class SwaggerConfig + { + public static void Register() + { + var thisAssembly = typeof(SwaggerConfig).Assembly; + + GlobalConfiguration.Configuration + .EnableSwagger(c => + { + // By default, the service root url is inferred from the request used to access the docs. + // However, there may be situations (e.g. proxy and load-balanced environments) where this does not + // resolve correctly. You can workaround this by providing your own code to determine the root URL. + // + //c.RootUrl(req => GetRootUrlFromAppConfig()); + + // If schemes are not explicitly provided in a Swagger 2.0 document, then the scheme used to access + // the docs is taken as the default. If your API supports multiple schemes and you want to be explicit + // about them, you can use the "Schemes" option as shown below. + // + //c.Schemes(new[] { "http", "https" }); + + // Use "SingleApiVersion" to describe a single version API. Swagger 2.0 includes an "Info" object to + // hold additional metadata for an API. Version and title are required but you can also provide + // additional fields by chaining methods off SingleApiVersion. + // + c.SingleApiVersion("v1", "WebApi.NetFx48"); + + // Taking to long to load the swagger docs? Enable this option to start caching it + // + //c.AllowCachingSwaggerDoc(); + + // If you want the output Swagger docs to be indented properly, enable the "PrettyPrint" option. + // + //c.PrettyPrint(); + + // If your API has multiple versions, use "MultipleApiVersions" instead of "SingleApiVersion". + // In this case, you must provide a lambda that tells Swagger-Net which actions should be + // included in the docs for a given API version. Like "SingleApiVersion", each call to "Version" + // returns an "Info" builder so you can provide additional metadata per API version. + // + //c.MultipleApiVersions( + // (apiDesc, targetApiVersion) => ResolveVersionSupportByRouteConstraint(apiDesc, targetApiVersion), + // (vc) => + // { + // vc.Version("v2", "Swagger-Net Dummy API V2"); + // vc.Version("v1", "Swagger-Net Dummy API V1"); + // }); + + // You can use "BasicAuth", "ApiKey" or "OAuth2" options to describe security schemes for the API. + // See https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md for more details. + // NOTE: These only define the schemes and need to be coupled with a corresponding "security" property + // at the document or operation level to indicate which schemes are required for an operation. To do this, + // you'll need to implement a custom IDocumentFilter and/or IOperationFilter to set these properties + // according to your specific authorization implementation + // + //c.BasicAuth("basic").Description("Basic HTTP Authentication"); + // + //c.ApiKey("apiKey", "header", "API Key Authentication"); + // + //c.OAuth2("oauth2") + // .Description("OAuth2 Implicit Grant") + // .Flow("implicit") + // .AuthorizationUrl("http://petstore.swagger.wordnik.com/api/oauth/dialog") + // //.TokenUrl("https://tempuri.org/token") + // .Scopes(scopes => + // { + // scopes.Add("read", "Read access to protected resources"); + // scopes.Add("write", "Write access to protected resources"); + // }); + + // Set this flag to omit descriptions for any actions decorated with the Obsolete attribute + //c.IgnoreObsoleteActions(); + + // Comment this setting to disable Access-Control-Allow-Origin + c.AccessControlAllowOrigin("*"); + + // Each operation be assigned one or more tags which are then used by consumers for various reasons. + // For example, the swagger-ui groups operations according to the first tag of each operation. + // By default, this will be controller name but you can use the "GroupActionsBy" option to + // override with any value. + // + //c.GroupActionsBy(apiDesc => apiDesc.HttpMethod.ToString()); + + // You can also specify a custom sort order for groups (as defined by "GroupActionsBy") to dictate + // the order in which operations are listed. For example, if the default grouping is in place + // (controller name) and you specify a descending alphabetic sort order, then actions from a + // ProductsController will be listed before those from a CustomersController. This is typically + // used to customize the order of groupings in the swagger-ui. + // + //c.OrderActionGroupsBy(new DescendingAlphabeticComparer()); + + // If you annotate Controllers and API Types with Xml comments: + // http://msdn.microsoft.com/en-us/library/b2s063f7(v=vs.110).aspx + // those comments will be incorporated into the generated docs and UI. + // Just make sure your comment file(s) have extension .XML + // You can add individual files by providing the path to one or + // more Xml comment files. + // + //c.IncludeXmlComments(AppDomain.CurrentDomain.BaseDirectory + "file.ext"); + c.IncludeAllXmlComments(thisAssembly, AppDomain.CurrentDomain.BaseDirectory); + + // Swagger-Net makes a best attempt at generating Swagger compliant JSON schemas for the various types + // exposed in your API. However, there may be occasions when more control of the output is needed. + // This is supported through the "MapType" and "SchemaFilter" options: + // + // Use the "MapType" option to override the Schema generation for a specific type. + // It should be noted that the resulting Schema will be placed "inline" for any applicable Operations. + // While Swagger 2.0 supports inline definitions for "all" Schema types, the swagger-ui tool does not. + // It expects "complex" Schemas to be defined separately and referenced. For this reason, you should only + // use the "MapType" option when the resulting Schema is a primitive or array type. If you need to alter a + // complex Schema, use a Schema filter. + // + //c.MapType(() => new Schema { type = "integer", format = "int32" }); + + // If you want to post-modify "complex" Schemas once they've been generated, across the board or for a + // specific type, you can wire up one or more Schema filters. + // + //c.SchemaFilter(); + + // In a Swagger 2.0 document, complex types are typically declared globally and referenced by unique + // Schema Id. By default, Swagger-Net does NOT use the full type name in Schema Ids. In most cases, this + // works well because it prevents the "implementation detail" of type namespaces from leaking into your + // Swagger docs and UI. However, if you have multiple types in your API with the same class name, you'll + // need to opt out of this behavior to avoid Schema Id conflicts. + // + //c.UseFullTypeNameInSchemaIds(); + + // Alternatively, you can provide your own custom strategy for inferring SchemaId's for + // describing "complex" types in your API. + // + //c.SchemaId(t => t.FullName.Contains('`') ? t.FullName.Substring(0, t.FullName.IndexOf('`')) : t.FullName); + + // Set this flag to omit schema property descriptions for any type properties decorated with the + // Obsolete attribute + //c.IgnoreObsoleteProperties(); + + // Set this flag to ignore IsSpecified members when serializing and deserializing types. + // + c.IgnoreIsSpecifiedMembers(); + + // In accordance with the built in JsonSerializer, if disabled Swagger-Net will describe enums as integers. + // You can change the serializer behavior by configuring the StringToEnumConverter globally or for a given + // enum type. Swagger-Net will honor this change out-of-the-box. However, if you use a different + // approach to serialize enums as strings, you can also force Swagger-Net to describe them as strings. + // + c.DescribeAllEnumsAsStrings(camelCase: false); + + // Similar to Schema filters, Swagger-Net also supports Operation and Document filters: + // + // Post-modify Operation descriptions once they've been generated by wiring up one or more + // Operation filters. + // + //c.OperationFilter(); + // + // If you've defined an OAuth2 flow as described above, you could use a custom filter + // to inspect some attribute on each action and infer which (if any) OAuth2 scopes are required + // to execute the operation + // + //c.OperationFilter(); + + // Post-modify the entire Swagger document by wiring up one or more Document filters. + // This gives full control to modify the final SwaggerDocument. You should have a good understanding of + // the Swagger 2.0 spec. - https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md + // before using this option. + // + //c.DocumentFilter(); + + // In contrast to WebApi, Swagger 2.0 does not include the query string component when mapping a URL + // to an action. As a result, Swagger-Net will raise an exception if it encounters multiple actions + // with the same path (sans query string) and HTTP method. You can workaround this by providing a + // custom strategy to pick a winner or merge the descriptions for the purposes of the Swagger docs + // + //c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); + + // Wrap the default SwaggerGenerator with additional behavior (e.g. caching) or provide an + // alternative implementation for ISwaggerProvider with the CustomProvider option. + // + //c.CustomProvider((defaultProvider) => new CachingSwaggerProvider(defaultProvider)); + }) + .EnableSwaggerUi(c => + { + // Use the "DocumentTitle" option to change the Document title. + // Very helpful when you have multiple Swagger pages open, to tell them apart. + // + //c.DocumentTitle("My Swagger UI"); + + // Use the "CssTheme" to add a theme to your UI. + // Options are: + // theme-feeling-blue-css + // theme-flattop-css + // theme-material-css + // theme-monokai-css + // theme-muted-css + // theme-newspaper-css + // theme-outline-css + // + //c.CssTheme(""); + + // Use the "InjectStylesheet" option to enrich the UI with one or more additional CSS stylesheets. + // The file must be included in your project as an "Embedded Resource", and then the resource's + // "Logical Name" is passed to the method as shown below. + // + //c.InjectStylesheet(thisAssembly, "Swagger.Net.Dummy.SwaggerExtensions.testStyles1.css"); + + // Use the "InjectJavaScript" option to invoke one or more custom JavaScripts after the swagger-ui + // has loaded. The file must be included in your project as an "Embedded Resource", and then the resource's + // "Logical Name" is passed to the method as shown above. + // + //c.InjectJavaScript(thisAssembly, "Swagger.Net.Dummy.SwaggerExtensions.testScript1.js"); + + // The swagger-ui renders boolean data types as a dropdown. By default, it provides "true" and "false" + // strings as the possible choices. You can use this option to change these to something else, + // for example 0 and 1. + // + //c.BooleanValues(new[] { "0", "1" }); + + // Controls the display of vendor extension (x-) fields and values for Operations, Parameters, and Schema. + // The default is true. + // + c.ShowExtensions(true); + + // Show pattern, minLength, maxLength, minimum, and maximum fields + // + //c.ShowCommonExtensions(true); + + // By default, swagger-ui will validate specs against swagger.io's online validator and display the result + // in a badge at the bottom of the page. Use these options to set a different validator URL or to disable the + // feature entirely. + c.SetValidatorUrl("https://online.swagger.io/validator"); + //c.DisableValidator(); + + // Use this option to control how the Operation listing is displayed. + // It can be set to "None" (default), "List" (shows operations for each resource), + // or "Full" (fully expanded: shows operations and their details). + // + //c.DocExpansion(DocExpansion.List); + + // Controls how models are shown when the API is first rendered. (The user can always switch + // the rendering for a given model by clicking the 'Model' and 'Example Value' links.) It can be + // set to 'model' or 'example', and the default is 'example'. + // + //c.DefaultModelRendering(DefaultModelRender.Model); + + // Use this option to control the expansion depth for the model on the model-example section. + // + //c.DefaultModelExpandDepth(0); + + // The default expansion depth for models (set to -1 completely hide the models). + // + //c.DefaultModelsExpandDepth(0); + + // Limit the number of operations shown to a smaller value + // + c.UImaxDisplayedTags(100); + + // Filter the operations works as a search, to disable set to "null" + // + c.UIfilter("''"); + + // Specify which HTTP operations will have the 'Try it out!' option. An empty parameter list disables + // it for all operations. + // + //c.SupportedSubmitMethods("GET", "HEAD"); + + // Use the CustomAsset option to provide your own version of assets used in the swagger-ui. + // It's typically used to instruct Swagger-Net to return your version instead of the default + // when a request is made for "index.html". As with all custom content, the file must be included + // in your project as an "Embedded Resource", and then the resource's "Logical Name" is passed to + // the method as shown below. + // + //c.CustomAsset("index", thisAssembly, "YourWebApiProject.SwaggerExtensions.index.html"); + + // If your API has multiple versions and you've applied the MultipleApiVersions setting + // as described above, you can also enable a select box in the swagger-ui, that displays + // a discovery URL for each version. This provides a convenient way for users to browse documentation + // for different API versions. + // + //c.EnableDiscoveryUrlSelector(); + + // If your API supports the OAuth2 Implicit flow, and you've described it correctly, according to + // the Swagger 2.0 specification, you can enable UI support as shown below. + // + //c.EnableOAuth2Support( + // clientId: "test-client-id", + // clientSecret: null, + // realm: "test-realm", + // appName: "Swagger UI" + // //additionalQueryStringParams: new Dictionary() { { "foo", "bar" } } + //); + }); + } + + public static bool ResolveVersionSupportByRouteConstraint(ApiDescription apiDesc, string targetApiVersion) + { + return (apiDesc.Route.RouteTemplate.ToLower().Contains(targetApiVersion.ToLower())); + } + + private class ApplyDocumentVendorExtensions : IDocumentFilter + { + public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer) + { + // Include the given data type in the final SwaggerDocument + // + //schemaRegistry.GetOrRegister(typeof(ExtraType)); + } + } + + public class AssignOAuth2SecurityRequirements : IOperationFilter + { + public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription) + { + // Correspond each "Authorize" role to an oauth2 scope + var scopes = apiDescription.ActionDescriptor.GetFilterPipeline() + .Select(filterInfo => filterInfo.Instance) + .OfType() + .SelectMany(attr => attr.Roles.Split(',')) + .Distinct(); + + if (scopes.Any()) + { + if (operation.security == null) + operation.security = new List>>(); + + var oAuthRequirements = new Dictionary> + { + { "oauth2", scopes } + }; + + operation.security.Add(oAuthRequirements); + } + } + } + + private class ApplySchemaVendorExtensions : ISchemaFilter + { + public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type) + { + // Modify the example values in the final SwaggerDocument + // + if (schema.properties != null) + { + foreach (var p in schema.properties) + { + switch (p.Value.format) + { + case "int32": + p.Value.example = 123; + break; + case "double": + p.Value.example = 9858.216; + break; + } + } + } + } + } + } +} diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/WebApiConfig.cs b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/WebApiConfig.cs new file mode 100644 index 00000000..3e6bc98c --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/App_Start/WebApiConfig.cs @@ -0,0 +1,28 @@ +using System.Web.Http; +using Swagger.Net.Application; + +namespace WebApi.NetFx48 +{ + public static class WebApiConfig + { + public static void Register(HttpConfiguration config) + { + // Web API configuration and services + + // Web API routes + config.MapHttpAttributeRoutes(); + + config.Routes.MapHttpRoute( + "DefaultApi", + "api/{controller}/{id}", + new {id = RouteParameter.Optional} + ); + config.Routes.MapHttpRoute( + "swagger_root", + "", + null, + null, + new RedirectHandler(message => message.RequestUri.ToString(), "swagger")); + } + } +} \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Controllers/DefaultController.cs b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Controllers/DefaultController.cs new file mode 100644 index 00000000..3c06576c --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Controllers/DefaultController.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using System.Web.Http; + +namespace WebApi.NetFx48.Controllers +{ + public class DefaultController : ApiController + { + public async Task Get() + { + return this.Ok("OK"); + } + } +} \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Global.asax b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Global.asax new file mode 100644 index 00000000..a51d9395 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Global.asax @@ -0,0 +1 @@ +<%@ Application Codebehind="Global.asax.cs" Inherits="WebApi.NetFx48.WebApiApplication" Language="C#" %> diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Global.asax.cs b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Global.asax.cs new file mode 100644 index 00000000..d10aca41 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Global.asax.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Http; +using System.Web.Routing; + +namespace WebApi.NetFx48 +{ + public class WebApiApplication : System.Web.HttpApplication + { + protected void Application_Start() + { + GlobalConfiguration.Configure(WebApiConfig.Register); + GlobalConfiguration.Configure(DependencyInjectionConfig.Register); + } + } +} diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Properties/AssemblyInfo.cs b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..c1d77883 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WebApi.NetFx48")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WebApi.NetFx48")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f405417b-110f-4a6d-849e-aacdde33f268")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Web.Debug.config b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Web.Debug.config new file mode 100644 index 00000000..fae9cfef --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Web.Debug.config @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Web.Release.config b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Web.Release.config new file mode 100644 index 00000000..da6e960b --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Web.Release.config @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Web.config b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Web.config new file mode 100644 index 00000000..e525e6f5 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/Web.config @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/WebApi.NetFx48.csproj b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/WebApi.NetFx48.csproj new file mode 100644 index 00000000..4684c477 --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/WebApi.NetFx48.csproj @@ -0,0 +1,207 @@ + + + + + Debug + AnyCPU + + + 2.0 + {F405417B-110F-4A6D-849E-AACDDE33F268} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + WebApi.NetFx48 + WebApi.NetFx48 + v4.8 + true + + 44306 + + + + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + true + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + ..\packages\Coravel.4.0.2\lib\netstandard2.0\Coravel.dll + + + ..\packages\FromHeaderAttribute.2.0.4\lib\net45\FromHeaderAttribute.dll + + + ..\packages\Microsoft.Bcl.AsyncInterfaces.5.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + + ..\packages\Microsoft.Extensions.Caching.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Caching.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Caching.Memory.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Caching.Memory.dll + + + ..\packages\Microsoft.Extensions.Configuration.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.Abstractions.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.5.0.1\lib\net461\Microsoft.Extensions.DependencyInjection.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.5.0.0\lib\net461\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\packages\Microsoft.Extensions.FileProviders.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.FileProviders.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Hosting.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Hosting.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Logging.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Options.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Options.dll + + + ..\packages\Microsoft.Extensions.Primitives.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll + + + ..\packages\Swagger-Net.8.3.44.1\lib\net45\Swagger.Net.dll + + + ..\packages\System.Buffers.4.4.0\lib\netstandard2.0\System.Buffers.dll + + + ..\packages\System.Memory.4.5.1\lib\netstandard2.0\System.Memory.dll + + + + + ..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + + + + + + + + + + + + + + + + + + + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + + + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll + + + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.7\lib\net45\System.Web.Http.dll + + + ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.7\lib\net45\System.Web.Http.WebHost.dll + + + ..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + + + + + + + + + + + + + + + Global.asax + + + + + + + Web.config + + + Web.config + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + True + True + 59101 + / + https://localhost:44306/ + False + False + + + False + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/WebApi.NetFx48.csproj.DotSettings b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/WebApi.NetFx48.csproj.DotSettings new file mode 100644 index 00000000..37e9881c --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/WebApi.NetFx48.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/packages.config b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/packages.config new file mode 100644 index 00000000..3bfe028e --- /dev/null +++ b/Coravel/Lab.CoravelScheduler/WebApi.NetFx48/packages.config @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DI/Lab.MsDI/Mvc5Net48/App_Start/DefaultDependencyResolver.cs b/DI/Lab.MsDI/Mvc5Net48/App_Start/DefaultDependencyResolver.cs index 18630899..ab391905 100644 --- a/DI/Lab.MsDI/Mvc5Net48/App_Start/DefaultDependencyResolver.cs +++ b/DI/Lab.MsDI/Mvc5Net48/App_Start/DefaultDependencyResolver.cs @@ -8,7 +8,6 @@ namespace Mvc5Net48 { internal class DefaultDependencyResolver : IDependencyResolver { - public object GetService(Type serviceType) { if (HttpContext.Current?.Items[typeof(IServiceScope)] is IServiceScope scope) diff --git a/DI/Lab.MsDI/Mvc5Net48/Message/IMessager.cs b/DI/Lab.MsDI/Mvc5Net48/Message/IMessager.cs index 8f3da6d6..e3bda957 100644 --- a/DI/Lab.MsDI/Mvc5Net48/Message/IMessager.cs +++ b/DI/Lab.MsDI/Mvc5Net48/Message/IMessager.cs @@ -1,6 +1,8 @@ -namespace Mvc5Net48.Message +using System; + +namespace Mvc5Net48.Message { - public interface IMessager + public interface IMessager:IDisposable { string OperationId { get; } } diff --git a/DI/Lab.MsDI/Mvc5Net48/Message/LogMessager.cs b/DI/Lab.MsDI/Mvc5Net48/Message/LogMessager.cs index 85dc6327..b40b1319 100644 --- a/DI/Lab.MsDI/Mvc5Net48/Message/LogMessager.cs +++ b/DI/Lab.MsDI/Mvc5Net48/Message/LogMessager.cs @@ -5,5 +5,10 @@ namespace Mvc5Net48.Message internal class LogMessager : IMessager { public string OperationId { get; } = $"日誌-{Guid.NewGuid()}"; + + public void Dispose() + { + Console.WriteLine($"{nameof(LogMessager)} GC"); + } } } \ No newline at end of file diff --git a/DI/Lab.MsDI/Mvc5Net48/Message/MachineMessager.cs b/DI/Lab.MsDI/Mvc5Net48/Message/MachineMessager.cs index 84fa31b8..8d91ec1d 100644 --- a/DI/Lab.MsDI/Mvc5Net48/Message/MachineMessager.cs +++ b/DI/Lab.MsDI/Mvc5Net48/Message/MachineMessager.cs @@ -5,5 +5,9 @@ namespace Mvc5Net48.Message internal class MachineMessager : IMessager { public string OperationId { get; } = $"機器-{Guid.NewGuid()}"; + public void Dispose() + { + Console.WriteLine($"{nameof(MachineMessager)} GC"); + } } } \ No newline at end of file diff --git a/DI/Lab.MsDI/Mvc5Net48/Message/MultiMessager.cs b/DI/Lab.MsDI/Mvc5Net48/Message/MultiMessager.cs index 1b155f0c..d2206ff8 100644 --- a/DI/Lab.MsDI/Mvc5Net48/Message/MultiMessager.cs +++ b/DI/Lab.MsDI/Mvc5Net48/Message/MultiMessager.cs @@ -5,5 +5,9 @@ namespace Mvc5Net48.Message public class MultiMessager : IScopeMessager, ISingleMessager, ITransientMessager { public string OperationId { get; } = $"多個接口-{Guid.NewGuid()}"; + public void Dispose() + { + Console.WriteLine($"{nameof(MultiMessager)} GC"); + } } } \ No newline at end of file diff --git a/DI/Lab.MsDI/Mvc5Net48/Web.config b/DI/Lab.MsDI/Mvc5Net48/Web.config index 417fbf99..58fb7220 100644 --- a/DI/Lab.MsDI/Mvc5Net48/Web.config +++ b/DI/Lab.MsDI/Mvc5Net48/Web.config @@ -28,6 +28,14 @@ + + + + + + + + diff --git a/DI/Lab.MsDI/WebApiNet48/App_Start/DefaultDependencyResolver.cs b/DI/Lab.MsDI/WebApiNet48/App_Start/DefaultDependencyResolver.cs index 6dc35633..e9e64035 100644 --- a/DI/Lab.MsDI/WebApiNet48/App_Start/DefaultDependencyResolver.cs +++ b/DI/Lab.MsDI/WebApiNet48/App_Start/DefaultDependencyResolver.cs @@ -7,33 +7,34 @@ namespace WebApiNet48 { public class DefaultDependencyResolver : IDependencyResolver { - protected IServiceProvider ServiceProvider { get; set; } + private readonly IServiceProvider _serviceProvider; + private IServiceScope _serviceScope; - public DefaultDependencyResolver(IServiceProvider serviceProvider) + public DefaultDependencyResolver(IServiceProvider serviceProvider, IServiceScope serviceScope = null) { - this.ServiceProvider = serviceProvider; + this._serviceProvider = serviceProvider; + this._serviceScope = serviceScope; } public object GetService(Type serviceType) { - return this.ServiceProvider.GetService(serviceType); + return this._serviceProvider.GetService(serviceType); } public IEnumerable GetServices(Type serviceType) { - return this.ServiceProvider.GetServices(serviceType); + return this._serviceProvider.GetServices(serviceType); } public IDependencyScope BeginScope() { - return new DefaultDependencyResolver(this.ServiceProvider.CreateScope().ServiceProvider); + this._serviceScope = this._serviceProvider.CreateScope(); + return new DefaultDependencyResolver(this._serviceScope.ServiceProvider,this._serviceScope); } public void Dispose() { - // you can implement this interface just when you use .net core 2.0 - // this.ServiceProvider.Dispose(); - ((ServiceProvider) this.ServiceProvider).Dispose(); + this._serviceScope?.Dispose(); } } } \ No newline at end of file diff --git a/DI/Lab.MsDI/WebApiNet48/Message/IMessager.cs b/DI/Lab.MsDI/WebApiNet48/Message/IMessager.cs index 137ae92c..9883346c 100644 --- a/DI/Lab.MsDI/WebApiNet48/Message/IMessager.cs +++ b/DI/Lab.MsDI/WebApiNet48/Message/IMessager.cs @@ -1,6 +1,8 @@ -namespace WebApiNet48 +using System; + +namespace WebApiNet48 { - public interface IMessager + public interface IMessager:IDisposable { string OperationId { get; } } diff --git a/DI/Lab.MsDI/WebApiNet48/Message/LogMessager.cs b/DI/Lab.MsDI/WebApiNet48/Message/LogMessager.cs index 91e273eb..a73f42f4 100644 --- a/DI/Lab.MsDI/WebApiNet48/Message/LogMessager.cs +++ b/DI/Lab.MsDI/WebApiNet48/Message/LogMessager.cs @@ -5,5 +5,10 @@ namespace WebApiNet48 internal class LogMessager : IMessager { public string OperationId { get; } = $"日誌-{Guid.NewGuid()}"; + + public void Dispose() + { + Console.WriteLine($"{nameof(LogMessager)} GC"); + } } } \ No newline at end of file diff --git a/DI/Lab.MsDI/WebApiNet48/Message/MachineMessager.cs b/DI/Lab.MsDI/WebApiNet48/Message/MachineMessager.cs index 39e04bc5..8aed0c19 100644 --- a/DI/Lab.MsDI/WebApiNet48/Message/MachineMessager.cs +++ b/DI/Lab.MsDI/WebApiNet48/Message/MachineMessager.cs @@ -5,5 +5,10 @@ namespace WebApiNet48 internal class MachineMessager : IMessager { public string OperationId { get; } = $"機器-{Guid.NewGuid()}"; + + public void Dispose() + { + Console.WriteLine($"{nameof(MachineMessager)} GC"); + } } } \ No newline at end of file diff --git a/DI/Lab.MsDI/WebApiNet48/Message/MultiMessager.cs b/DI/Lab.MsDI/WebApiNet48/Message/MultiMessager.cs index 17d01e6c..7e3edf7b 100644 --- a/DI/Lab.MsDI/WebApiNet48/Message/MultiMessager.cs +++ b/DI/Lab.MsDI/WebApiNet48/Message/MultiMessager.cs @@ -5,5 +5,10 @@ namespace WebApiNet48 public class MultiMessager : IScopeMessager, ISingleMessager, ITransientMessager { public string OperationId { get; } = $"多個接口-{Guid.NewGuid()}"; + + public void Dispose() + { + Console.WriteLine($"{nameof(MultiMessager)} GC"); + } } } \ No newline at end of file diff --git a/DI/Lab.MsDI/WebApiNetCore31/Message/IMessager.cs b/DI/Lab.MsDI/WebApiNetCore31/Message/IMessager.cs index 07685fa7..479144a9 100644 --- a/DI/Lab.MsDI/WebApiNetCore31/Message/IMessager.cs +++ b/DI/Lab.MsDI/WebApiNetCore31/Message/IMessager.cs @@ -1,6 +1,8 @@ -namespace WebApiNetCore31 +using System; + +namespace WebApiNetCore31 { - public interface IMessager + public interface IMessager:IDisposable { string OperationId { get; } } diff --git a/DI/Lab.MsDI/WebApiNetCore31/Message/LogMessager.cs b/DI/Lab.MsDI/WebApiNetCore31/Message/LogMessager.cs index 633adcba..9819ac66 100644 --- a/DI/Lab.MsDI/WebApiNetCore31/Message/LogMessager.cs +++ b/DI/Lab.MsDI/WebApiNetCore31/Message/LogMessager.cs @@ -5,5 +5,10 @@ namespace WebApiNetCore31 internal class LogMessager : IMessager { public string OperationId { get; } = $"日誌-{Guid.NewGuid()}"; + + public void Dispose() + { + Console.WriteLine($"{nameof(LogMessager)} GC"); + } } } \ No newline at end of file diff --git a/DI/Lab.MsDI/WebApiNetCore31/Message/MachineMessager.cs b/DI/Lab.MsDI/WebApiNetCore31/Message/MachineMessager.cs index d3e4038f..ed49b5dd 100644 --- a/DI/Lab.MsDI/WebApiNetCore31/Message/MachineMessager.cs +++ b/DI/Lab.MsDI/WebApiNetCore31/Message/MachineMessager.cs @@ -5,5 +5,9 @@ namespace WebApiNetCore31 internal class MachineMessager : IMessager { public string OperationId { get; } = $"機器-{Guid.NewGuid()}"; + public void Dispose() + { + Console.WriteLine($"{nameof(MachineMessager)} GC"); + } } } \ No newline at end of file diff --git a/DI/Lab.MsDI/WebApiNetCore31/Message/MultiMessager.cs b/DI/Lab.MsDI/WebApiNetCore31/Message/MultiMessager.cs index e4eb78a9..68182ed0 100644 --- a/DI/Lab.MsDI/WebApiNetCore31/Message/MultiMessager.cs +++ b/DI/Lab.MsDI/WebApiNetCore31/Message/MultiMessager.cs @@ -5,5 +5,10 @@ namespace WebApiNetCore31 public class MultiMessager : IScopeMessager, ISingleMessager, ITransientMessager { public string OperationId { get; } = $"多個接口-{Guid.NewGuid()}"; + + public void Dispose() + { + Console.WriteLine($"{nameof(MultiMessager)} GC"); + } } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/Lab.MsDIForAutofac.sln b/DI/Lab.MsDIForAutofac/Lab.MsDIForAutofac.sln index 7a83e88b..b710049a 100644 --- a/DI/Lab.MsDIForAutofac/Lab.MsDIForAutofac.sln +++ b/DI/Lab.MsDIForAutofac/Lab.MsDIForAutofac.sln @@ -1,31 +1,31 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30503.244 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiNetCore31", "WebApiNetCore31\WebApiNetCore31.csproj", "{B5E0DA79-326E-41CB-A95C-0C7FFDC70FF0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiNet48", "WebApiNet48\WebApiNet48.csproj", "{9EA9B67E-7812-41CB-899B-4331B5344882}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B5E0DA79-326E-41CB-A95C-0C7FFDC70FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B5E0DA79-326E-41CB-A95C-0C7FFDC70FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B5E0DA79-326E-41CB-A95C-0C7FFDC70FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B5E0DA79-326E-41CB-A95C-0C7FFDC70FF0}.Release|Any CPU.Build.0 = Release|Any CPU - {9EA9B67E-7812-41CB-899B-4331B5344882}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9EA9B67E-7812-41CB-899B-4331B5344882}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9EA9B67E-7812-41CB-899B-4331B5344882}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9EA9B67E-7812-41CB-899B-4331B5344882}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {526A154E-E406-4F6B-A76D-4455CA7B02B1} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30503.244 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiNetCore31", "WebApiNetCore31\WebApiNetCore31.csproj", "{B5E0DA79-326E-41CB-A95C-0C7FFDC70FF0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiNet48", "WebApiNet48\WebApiNet48.csproj", "{9EA9B67E-7812-41CB-899B-4331B5344882}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B5E0DA79-326E-41CB-A95C-0C7FFDC70FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5E0DA79-326E-41CB-A95C-0C7FFDC70FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5E0DA79-326E-41CB-A95C-0C7FFDC70FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5E0DA79-326E-41CB-A95C-0C7FFDC70FF0}.Release|Any CPU.Build.0 = Release|Any CPU + {9EA9B67E-7812-41CB-899B-4331B5344882}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9EA9B67E-7812-41CB-899B-4331B5344882}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9EA9B67E-7812-41CB-899B-4331B5344882}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9EA9B67E-7812-41CB-899B-4331B5344882}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {526A154E-E406-4F6B-A76D-4455CA7B02B1} + EndGlobalSection +EndGlobal diff --git a/DI/Lab.MsDIForAutofac/TestProject1/Hello.cs b/DI/Lab.MsDIForAutofac/TestProject1/Hello.cs new file mode 100644 index 00000000..6feacc21 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/TestProject1/Hello.cs @@ -0,0 +1,23 @@ +namespace TestProject1 +{ + public interface IHello + { + string SayHello(); + } + + public class EnglishHello : IHello + { + public string SayHello() + { + return "Hello"; + } + } + + public class FrenchHello : IHello + { + public string SayHello() + { + return "Bonjour"; + } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/TestProject1/HelloConsumer.cs b/DI/Lab.MsDIForAutofac/TestProject1/HelloConsumer.cs new file mode 100644 index 00000000..b20ad097 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/TestProject1/HelloConsumer.cs @@ -0,0 +1,24 @@ +using System; +using Autofac.Features.AttributeFilters; + +namespace TestProject1 +{ + public class HelloConsumer + { + private readonly IHello helloService; + + public HelloConsumer([KeyFilter("FR")] IHello helloService) + { + if (helloService == null) + { + throw new ArgumentNullException("helloService"); + } + this.helloService = helloService; + } + + public string SayHello() + { + return this.helloService.SayHello(); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/TestProject1/TestProject1.csproj b/DI/Lab.MsDIForAutofac/TestProject1/TestProject1.csproj new file mode 100644 index 00000000..a9ae9b11 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/TestProject1/TestProject1.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + + false + + + + + + + + + + + diff --git a/DI/Lab.MsDIForAutofac/TestProject1/UnitTest1.cs b/DI/Lab.MsDIForAutofac/TestProject1/UnitTest1.cs new file mode 100644 index 00000000..f5c735c4 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/TestProject1/UnitTest1.cs @@ -0,0 +1,25 @@ +using System; +using Autofac; +using Autofac.Features.AttributeFilters; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace TestProject1 +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + ContainerBuilder cb = new ContainerBuilder(); + + cb.RegisterType().Keyed("EN"); + cb.RegisterType().Keyed("FR"); + cb.RegisterType().WithAttributeFiltering(); + var container = cb.Build(); + + var consumer = container.Resolve(); + Console.WriteLine(consumer.SayHello()); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/UnitTestProject1/App.config b/DI/Lab.MsDIForAutofac/UnitTestProject1/App.config new file mode 100644 index 00000000..3002dd1f --- /dev/null +++ b/DI/Lab.MsDIForAutofac/UnitTestProject1/App.config @@ -0,0 +1,30 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/UnitTestProject1/EntityModel/IDENTITY.cs b/DI/Lab.MsDIForAutofac/UnitTestProject1/EntityModel/IDENTITY.cs new file mode 100644 index 00000000..cd16740a --- /dev/null +++ b/DI/Lab.MsDIForAutofac/UnitTestProject1/EntityModel/IDENTITY.cs @@ -0,0 +1,39 @@ +using LinqToDB.Mapping; + +namespace UnitTestProject1.EntityModel +{ + /// + /// + [Table("IDENTITY")] + public class Identity + { + /// + /// MEMBER_ID + /// + [Column] + [PrimaryKey] + public int MEMBER_ID { get; set; } + + /// + /// ACCOUNT + /// + [Column] + public string ACCOUNT { get; set; } + + /// + /// PASSWORD + /// + [Column] + public string PASSWORD { get; set; } + + /// + /// REMARK + /// + [Column] + [Nullable] + public string REMARK { get; set; } + + [Association(ThisKey = "MEMBER_ID", OtherKey = "ID", CanBeNull = false)] + public Member MEMBER { get; set; } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/UnitTestProject1/EntityModel/MEMBER.cs b/DI/Lab.MsDIForAutofac/UnitTestProject1/EntityModel/MEMBER.cs new file mode 100644 index 00000000..1621a3eb --- /dev/null +++ b/DI/Lab.MsDIForAutofac/UnitTestProject1/EntityModel/MEMBER.cs @@ -0,0 +1,39 @@ +using LinqToDB.Mapping; + +namespace UnitTestProject1.EntityModel +{ + /// + /// + [Table("MEMBER")] + public class Member + { + /// + /// ID + /// + [PrimaryKey] + [Column] + public int ID { get; set; } + + /// + /// NAME + /// + [Column] + public string NAME { get; set; } + + /// + /// AGE + /// + [Column] + public int AGE { get; set; } + + /// + /// REMARK + /// + [Column] + [Nullable] + public string REMARK { get; set; } + + [Association(ThisKey = "ID", OtherKey = "MEMBER_ID", CanBeNull = true)] + public Identity IDENTITY { get; set; } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/UnitTestProject1/EntityModel/MemberDb.cs b/DI/Lab.MsDIForAutofac/UnitTestProject1/EntityModel/MemberDb.cs new file mode 100644 index 00000000..6045d451 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/UnitTestProject1/EntityModel/MemberDb.cs @@ -0,0 +1,24 @@ +using LinqToDB; +using LinqToDB.Data; + +namespace UnitTestProject1.EntityModel +{ + public class MemberDb : DataConnection + { + public MemberDb() + : base("MemberDb") + { + } + + //public ITable MJVNTRs { get { return this.GetTable(); } } + public ITable Members + { + get { return this.GetTable(); } + } + + public ITable Identities + { + get { return this.GetTable(); } + } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/UnitTestProject1/Properties/AssemblyInfo.cs b/DI/Lab.MsDIForAutofac/UnitTestProject1/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..80f817bf --- /dev/null +++ b/DI/Lab.MsDIForAutofac/UnitTestProject1/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("UnitTestProject1")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("UnitTestProject1")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("79bb1d0c-74b0-4b0f-acab-251831bfc96c")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/DI/Lab.MsDIForAutofac/UnitTestProject1/UnitTest1.cs b/DI/Lab.MsDIForAutofac/UnitTestProject1/UnitTest1.cs new file mode 100644 index 00000000..04a35b9f --- /dev/null +++ b/DI/Lab.MsDIForAutofac/UnitTestProject1/UnitTest1.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using UnitTestProject1.EntityModel; + +namespace UnitTestProject1 +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + using (var db=new MemberDb()) + { + var members = db.Members.ToList(); + } + } + + } +} diff --git a/DI/Lab.MsDIForAutofac/UnitTestProject1/UnitTestProject1.csproj b/DI/Lab.MsDIForAutofac/UnitTestProject1/UnitTestProject1.csproj new file mode 100644 index 00000000..0ba22660 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/UnitTestProject1/UnitTestProject1.csproj @@ -0,0 +1,88 @@ + + + + + + Debug + AnyCPU + {79BB1D0C-74B0-4B0F-ACAB-251831BFC96C} + Library + Properties + UnitTestProject1 + UnitTestProject1 + v4.8 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + C:\Program Files (x86)\IBM\Client Access\IBM.Data.DB2.iSeries.dll + + + ..\packages\linq2db.2.6.0\lib\net46\linq2db.dll + + + ..\packages\linq2db4iSeries.2.6.0\lib\net45\LinqToDB.DataProvider.DB2iSeries.dll + + + + ..\packages\MSTest.TestFramework.2.1.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.2.1.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/UnitTestProject1/packages.config b/DI/Lab.MsDIForAutofac/UnitTestProject1/packages.config new file mode 100644 index 00000000..692b8154 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/UnitTestProject1/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/DefaultDependencyResolver.cs b/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/DefaultDependencyResolver.cs index ea9b067e..738882bb 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/DefaultDependencyResolver.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/DefaultDependencyResolver.cs @@ -1,39 +1,39 @@ -using System; -using System.Collections.Generic; -using System.Web.Http.Dependencies; -using Microsoft.Extensions.DependencyInjection; - -namespace WebApiNet48 -{ - public class DefaultDependencyResolver : IDependencyResolver - { - protected IServiceProvider ServiceProvider { get; set; } - - public DefaultDependencyResolver(IServiceProvider serviceProvider) - { - this.ServiceProvider = serviceProvider; - } - - public object GetService(Type serviceType) - { - return this.ServiceProvider.GetService(serviceType); - } - - public IEnumerable GetServices(Type serviceType) - { - return this.ServiceProvider.GetServices(serviceType); - } - - public IDependencyScope BeginScope() - { - return new DefaultDependencyResolver(this.ServiceProvider.CreateScope().ServiceProvider); - } - - public void Dispose() - { - // you can implement this interface just when you use .net core 2.0 - // this.ServiceProvider.Dispose(); - ((ServiceProvider)this.ServiceProvider).Dispose(); - } - } +using System; +using System.Collections.Generic; +using System.Web.Http.Dependencies; +using Microsoft.Extensions.DependencyInjection; + +namespace WebApiNet48 +{ + public class DefaultDependencyResolver : IDependencyResolver + { + protected IServiceProvider ServiceProvider { get; set; } + + public DefaultDependencyResolver(IServiceProvider serviceProvider) + { + this.ServiceProvider = serviceProvider; + } + + public object GetService(Type serviceType) + { + return this.ServiceProvider.GetService(serviceType); + } + + public IEnumerable GetServices(Type serviceType) + { + return this.ServiceProvider.GetServices(serviceType); + } + + public IDependencyScope BeginScope() + { + return new DefaultDependencyResolver(this.ServiceProvider.CreateScope().ServiceProvider); + } + + public void Dispose() + { + // you can implement this interface just when you use .net core 2.0 + // this.ServiceProvider.Dispose(); + ((ServiceProvider)this.ServiceProvider).Dispose(); + } + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/DependencyInjectionConfig.cs b/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/DependencyInjectionConfig.cs index 343599eb..6e2d792a 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/DependencyInjectionConfig.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/DependencyInjectionConfig.cs @@ -1,67 +1,67 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Web.Http; -using System.Web.Http.Controllers; -using Autofac; -using Autofac.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection; - -namespace WebApiNet48 -{ - public class DependencyInjectionConfig - { - public static void Register(HttpConfiguration config) - { - var services = ConfigureServices(); - var builder = ConfigureContainerBuilder(services); - var provider = new AutofacServiceProvider(builder.Build()); - - //var provider = services.BuildServiceProvider(); - - var resolver = new DefaultDependencyResolver(provider); - config.DependencyResolver = resolver; - } - - /// - /// 使用 Autofac 註冊 - /// - /// - /// - private static ContainerBuilder ConfigureContainerBuilder(IServiceCollection services) - { - var builder = new ContainerBuilder(); - builder.Populate(services); - - var assembly = Assembly.GetExecutingAssembly(); - builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces(); - - return builder; - } - - /// - /// 使用 MS DI 註冊 - /// - /// - private static ServiceCollection ConfigureServices() - { - var services = new ServiceCollection(); - - //使用 Microsoft.Extensions.DependencyInjection 註冊 - services.AddControllersAsServices(typeof(DependencyInjectionConfig) - .Assembly - .GetExportedTypes() - .Where(t => !t.IsAbstract && !t.IsGenericTypeDefinition) - .Where(t => typeof(IHttpController).IsAssignableFrom(t) - || t.Name.EndsWith("Controller", - StringComparison.OrdinalIgnoreCase))); - - //services.AddScoped(); - - //services.AddTransient() - // .AddSingleton() - // .AddScoped(); - return services; - } - } +using System; +using System.Linq; +using System.Reflection; +using System.Web.Http; +using System.Web.Http.Controllers; +using Autofac; +using Autofac.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace WebApiNet48 +{ + public class DependencyInjectionConfig + { + public static void Register(HttpConfiguration config) + { + var services = ConfigureServices(); + var builder = ConfigureContainerBuilder(services); + var provider = new AutofacServiceProvider(builder.Build()); + + //var provider = services.BuildServiceProvider(); + + var resolver = new DefaultDependencyResolver(provider); + config.DependencyResolver = resolver; + } + + /// + /// 使用 Autofac 註冊 + /// + /// + /// + private static ContainerBuilder ConfigureContainerBuilder(IServiceCollection services) + { + var builder = new ContainerBuilder(); + builder.Populate(services); + + var assembly = Assembly.GetExecutingAssembly(); + builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces(); + + return builder; + } + + /// + /// 使用 MS DI 註冊 + /// + /// + private static ServiceCollection ConfigureServices() + { + var services = new ServiceCollection(); + + //使用 Microsoft.Extensions.DependencyInjection 註冊 + services.AddControllersAsServices(typeof(DependencyInjectionConfig) + .Assembly + .GetExportedTypes() + .Where(t => !t.IsAbstract && !t.IsGenericTypeDefinition) + .Where(t => typeof(IHttpController).IsAssignableFrom(t) + || t.Name.EndsWith("Controller", + StringComparison.OrdinalIgnoreCase))); + + //services.AddScoped(); + + //services.AddTransient() + // .AddSingleton() + // .AddScoped(); + return services; + } + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/ServiceProviderExtensions.cs b/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/ServiceProviderExtensions.cs index 614d3991..eda67822 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/ServiceProviderExtensions.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/ServiceProviderExtensions.cs @@ -1,20 +1,20 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; - -namespace WebApiNet48 -{ - public static class ServiceProviderExtensions - { - public static IServiceCollection AddControllersAsServices(this IServiceCollection services, - IEnumerable controllerTypes) - { - foreach (var type in controllerTypes) - { - services.AddTransient(type); - } - - return services; - } - } +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; + +namespace WebApiNet48 +{ + public static class ServiceProviderExtensions + { + public static IServiceCollection AddControllersAsServices(this IServiceCollection services, + IEnumerable controllerTypes) + { + foreach (var type in controllerTypes) + { + services.AddTransient(type); + } + + return services; + } + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/WebApiConfig.cs b/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/WebApiConfig.cs index cd67649d..8ee1c430 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/WebApiConfig.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/App_Start/WebApiConfig.cs @@ -1,25 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Http; - -namespace WebApiNet48 -{ - public static class WebApiConfig - { - public static void Register(HttpConfiguration config) - { - DependencyInjectionConfig.Register(config); - // Web API configuration and services - - // Web API routes - config.MapHttpAttributeRoutes(); - - config.Routes.MapHttpRoute( - name: "DefaultApi", - routeTemplate: "api/{controller}/{id}", - defaults: new { id = RouteParameter.Optional } - ); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Http; + +namespace WebApiNet48 +{ + public static class WebApiConfig + { + public static void Register(HttpConfiguration config) + { + DependencyInjectionConfig.Register(config); + // Web API configuration and services + + // Web API routes + config.MapHttpAttributeRoutes(); + + config.Routes.MapHttpRoute( + name: "DefaultApi", + routeTemplate: "api/{controller}/{id}", + defaults: new { id = RouteParameter.Optional } + ); + } + } +} diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/Controllers/DefaultController.cs b/DI/Lab.MsDIForAutofac/WebApiNet48/Controllers/DefaultController.cs index a2030733..9f16bc34 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/Controllers/DefaultController.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/Controllers/DefaultController.cs @@ -1,36 +1,36 @@ -using System.Net.Http; -using System.Web.Http; - -namespace WebApiNet48.Controllers -{ - public class DefaultController : ApiController - { - private IMessager Messager { get; set; } - - public DefaultController(IMessager messager) - { - this.Messager = messager; - } - - [HttpGet] - public IHttpActionResult Get() - { - var content = $"Messager:{this.Messager.OperationId}"; - return this.Ok(content); - } - - [HttpGet] - public IHttpActionResult Get1() - { - var messager = InstanceManager.Messager; - - var content = $"Messager:{messager.OperationId}"; - return this.Ok(content); - } - } - - public class InstanceManager - { - public static IMessager Messager { get; set; } - } +using System.Net.Http; +using System.Web.Http; + +namespace WebApiNet48.Controllers +{ + public class DefaultController : ApiController + { + private IMessager Messager { get; set; } + + public DefaultController(IMessager messager) + { + this.Messager = messager; + } + + [HttpGet] + public IHttpActionResult Get() + { + var content = $"Messager:{this.Messager.OperationId}"; + return this.Ok(content); + } + + [HttpGet] + public IHttpActionResult Get1() + { + var messager = InstanceManager.Messager; + + var content = $"Messager:{messager.OperationId}"; + return this.Ok(content); + } + } + + public class InstanceManager + { + public static IMessager Messager { get; set; } + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/Global.asax b/DI/Lab.MsDIForAutofac/WebApiNet48/Global.asax index 593fb3aa..7946eef2 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/Global.asax +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/Global.asax @@ -1 +1 @@ -<%@ Application Codebehind="Global.asax.cs" Inherits="WebApiNet48.WebApiApplication" Language="C#" %> +<%@ Application Codebehind="Global.asax.cs" Inherits="WebApiNet48.WebApiApplication" Language="C#" %> diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/Global.asax.cs b/DI/Lab.MsDIForAutofac/WebApiNet48/Global.asax.cs index d3bb6c7b..13555dcb 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/Global.asax.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/Global.asax.cs @@ -1,17 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.Http; -using System.Web.Routing; - -namespace WebApiNet48 -{ - public class WebApiApplication : System.Web.HttpApplication - { - protected void Application_Start() - { - GlobalConfiguration.Configure(WebApiConfig.Register); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Http; +using System.Web.Routing; + +namespace WebApiNet48 +{ + public class WebApiApplication : System.Web.HttpApplication + { + protected void Application_Start() + { + GlobalConfiguration.Configure(WebApiConfig.Register); + } + } +} diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/Message/IMessager.cs b/DI/Lab.MsDIForAutofac/WebApiNet48/Message/IMessager.cs index 137ae92c..8544b3b9 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/Message/IMessager.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/Message/IMessager.cs @@ -1,7 +1,7 @@ -namespace WebApiNet48 -{ - public interface IMessager - { - string OperationId { get; } - } +namespace WebApiNet48 +{ + public interface IMessager + { + string OperationId { get; } + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/Message/LogMessager.cs b/DI/Lab.MsDIForAutofac/WebApiNet48/Message/LogMessager.cs index 91e273eb..55bac639 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/Message/LogMessager.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/Message/LogMessager.cs @@ -1,9 +1,9 @@ -using System; - -namespace WebApiNet48 -{ - internal class LogMessager : IMessager - { - public string OperationId { get; } = $"日誌-{Guid.NewGuid()}"; - } +using System; + +namespace WebApiNet48 +{ + internal class LogMessager : IMessager + { + public string OperationId { get; } = $"日誌-{Guid.NewGuid()}"; + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/Properties/AssemblyInfo.cs b/DI/Lab.MsDIForAutofac/WebApiNet48/Properties/AssemblyInfo.cs index e6621e86..09e000e3 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/Properties/AssemblyInfo.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/Properties/AssemblyInfo.cs @@ -1,35 +1,35 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("WebApiNet48")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("WebApiNet48")] -[assembly: AssemblyCopyright("Copyright © 2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("9ea9b67e-7812-41cb-899b-4331b5344882")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Revision and Build Numbers -// by using the '*' as shown below: -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WebApiNet48")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WebApiNet48")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9ea9b67e-7812-41cb-899b-4331b5344882")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/ServiceCollectionExtensions.cs b/DI/Lab.MsDIForAutofac/WebApiNet48/ServiceCollectionExtensions.cs index 7f5ea374..3fe60da6 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/ServiceCollectionExtensions.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/ServiceCollectionExtensions.cs @@ -1,26 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using Autofac; -using Autofac.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection; - -namespace WebApiNet48 -{ - public static class ServiceCollectionExtensions - { - /// - /// Adds the to the service collection. ONLY FOR PRE-ASP.NET 3.0 HOSTING. THIS WON'T WORK - /// FOR ASP.NET CORE 3.0+ OR GENERIC HOSTING. - /// - /// The service collection to add the factory to. - /// Action on a that adds component registrations to the container. - /// The service collection. - public static IServiceCollection AddAutofac(this IServiceCollection services, Action configurationAction = null) - { - return services.AddSingleton>(new AutofacServiceProviderFactory(configurationAction)); - } - } - +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Autofac; +using Autofac.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace WebApiNet48 +{ + public static class ServiceCollectionExtensions + { + /// + /// Adds the to the service collection. ONLY FOR PRE-ASP.NET 3.0 HOSTING. THIS WON'T WORK + /// FOR ASP.NET CORE 3.0+ OR GENERIC HOSTING. + /// + /// The service collection to add the factory to. + /// Action on a that adds component registrations to the container. + /// The service collection. + public static IServiceCollection AddAutofac(this IServiceCollection services, Action configurationAction = null) + { + return services.AddSingleton>(new AutofacServiceProviderFactory(configurationAction)); + } + } + } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/Web.Debug.config b/DI/Lab.MsDIForAutofac/WebApiNet48/Web.Debug.config index fae9cfef..c1a56423 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/Web.Debug.config +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/Web.Debug.config @@ -1,30 +1,30 @@ - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/Web.Release.config b/DI/Lab.MsDIForAutofac/WebApiNet48/Web.Release.config index da6e960b..19058ed3 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/Web.Release.config +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/Web.Release.config @@ -1,31 +1,31 @@ - - - - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/Web.config b/DI/Lab.MsDIForAutofac/WebApiNet48/Web.config index 89be7bd4..2a9874d6 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/Web.config +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/Web.config @@ -1,58 +1,58 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/WebApiNet48.csproj b/DI/Lab.MsDIForAutofac/WebApiNet48/WebApiNet48.csproj index f8a73be7..dd49038d 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/WebApiNet48.csproj +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/WebApiNet48.csproj @@ -1,184 +1,184 @@ - - - - - Debug - AnyCPU - - - 2.0 - {9EA9B67E-7812-41CB-899B-4331B5344882} - {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties - WebApiNet48 - WebApiNet48 - v4.8 - true - - 44327 - - - - - - - - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - - - true - pdbonly - true - bin\ - TRACE - prompt - 4 - - - - ..\packages\Autofac.6.0.0\lib\netstandard2.0\Autofac.dll - - - ..\packages\Autofac.Extensions.DependencyInjection.7.1.0\lib\netstandard2.0\Autofac.Extensions.DependencyInjection.dll - - - ..\packages\Microsoft.Bcl.AsyncInterfaces.1.1.1\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll - - - - ..\packages\Microsoft.Extensions.DependencyInjection.3.1.9\lib\net461\Microsoft.Extensions.DependencyInjection.dll - - - ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.3.1.9\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll - - - ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - - ..\packages\System.Diagnostics.DiagnosticSource.4.7.1\lib\net46\System.Diagnostics.DiagnosticSource.dll - - - ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll - - - - - ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - - - ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll - - - - - - - - - - - - - - - - - - - - - ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll - - - ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll - - - ..\packages\Microsoft.AspNet.WebApi.Core.5.2.7\lib\net45\System.Web.Http.dll - - - ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.7\lib\net45\System.Web.Http.WebHost.dll - - - ..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll - - - - - - - - - - - - - - - - Global.asax - - - - - - - Web.config - - - Web.config - - - - - - - - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - - - - - True - True - 54526 - / - https://localhost:44327/ - False - False - - - False - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - + + + + + Debug + AnyCPU + + + 2.0 + {9EA9B67E-7812-41CB-899B-4331B5344882} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + WebApiNet48 + WebApiNet48 + v4.8 + true + + 44327 + + + + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + true + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + ..\packages\Autofac.6.0.0\lib\netstandard2.0\Autofac.dll + + + ..\packages\Autofac.Extensions.DependencyInjection.7.1.0\lib\netstandard2.0\Autofac.Extensions.DependencyInjection.dll + + + ..\packages\Microsoft.Bcl.AsyncInterfaces.1.1.1\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + + ..\packages\Microsoft.Extensions.DependencyInjection.3.1.9\lib\net461\Microsoft.Extensions.DependencyInjection.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.3.1.9\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + ..\packages\System.Diagnostics.DiagnosticSource.4.7.1\lib\net46\System.Diagnostics.DiagnosticSource.dll + + + ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + + + + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + + + + + + + + + + + + + + + + + + + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + + + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll + + + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.7\lib\net45\System.Web.Http.dll + + + ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.7\lib\net45\System.Web.Http.WebHost.dll + + + ..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + + + + + + + + + + + + + + + + Global.asax + + + + + + + Web.config + + + Web.config + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + True + True + 54526 + / + https://localhost:44327/ + False + False + + + False + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/WebApiNet48.csproj.user b/DI/Lab.MsDIForAutofac/WebApiNet48/WebApiNet48.csproj.user new file mode 100644 index 00000000..9749d8ec --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/WebApiNet48.csproj.user @@ -0,0 +1,45 @@ + + + + true + + 44327 + + + + + Debug|Any CPU + ApiControllerEmptyScaffolder + root/Controller + 600 + True + False + True + + False + + + + + + api/default + SpecificPage + True + False + False + False + + + + + + + + + True + False + + + + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/obj/Debug/DesignTimeResolveAssemblyReferencesInput.cache b/DI/Lab.MsDIForAutofac/WebApiNet48/obj/Debug/DesignTimeResolveAssemblyReferencesInput.cache new file mode 100644 index 00000000..226d11bd Binary files /dev/null and b/DI/Lab.MsDIForAutofac/WebApiNet48/obj/Debug/DesignTimeResolveAssemblyReferencesInput.cache differ diff --git a/DI/Lab.MsDIForAutofac/WebApiNet48/packages.config b/DI/Lab.MsDIForAutofac/WebApiNet48/packages.config index f90d917a..7d6100d0 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNet48/packages.config +++ b/DI/Lab.MsDIForAutofac/WebApiNet48/packages.config @@ -1,20 +1,20 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Controllers/AutofacDefaultController.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Controllers/AutofacDefaultController.cs new file mode 100644 index 00000000..cb8c33b4 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Controllers/AutofacDefaultController.cs @@ -0,0 +1,29 @@ +using Autofac.Features.AttributeFilters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace WebApiNetCore31.Controllers +{ + [ApiController] + [Route("[controller]")] + public class AutofacDefaultController : ControllerBase + { + private readonly IFileProvider _fileProvider; + + private readonly ILogger _logger; + + + public AutofacDefaultController(ILogger logger, + [KeyFilter("zip")] IFileProvider fileProvider) + { + this._logger = logger; + this._fileProvider = fileProvider; + } + + [HttpGet] + public IActionResult Get() + { + return this.Ok(this._fileProvider.Print()); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Controllers/DefaultController.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Controllers/DefaultController.cs index 3aa6d5a3..8402dcb9 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Controllers/DefaultController.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Controllers/DefaultController.cs @@ -1,30 +1,30 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace WebApiNetCore31.Controllers -{ - [ApiController] - [Route("[controller]")] - public class DefaultController : ControllerBase - { - private IMessager Messager { get; } - - private readonly ILogger _logger; - - public DefaultController(ILogger logger, - IMessager messager - ) - { - this._logger = logger; - this.Messager = messager; - } - - [HttpGet] - public IActionResult Get() - { - var content = $"Messager:{this.Messager.OperationId}"; - this._logger.LogInformation("Messager:{message}", content); - return this.Ok(content); - } - } +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace WebApiNetCore31.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DefaultController : ControllerBase + { + private IMessager Messager { get; } + + private readonly ILogger _logger; + + public DefaultController(ILogger logger, + IMessager messager + ) + { + this._logger = logger; + this.Messager = messager; + } + + [HttpGet] + public IActionResult Get() + { + var content = $"Messager:{this.Messager.OperationId}"; + this._logger.LogInformation("Messager:{message}", content); + return this.Ok(content); + } + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Controllers/TestController.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Controllers/TestController.cs new file mode 100644 index 00000000..aafcda8e --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Controllers/TestController.cs @@ -0,0 +1,27 @@ +using Autofac.Features.AttributeFilters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace WebApiNetCore31.Controllers +{ + [ApiController] + [Route("[controller]")] + public class TestController : ControllerBase + { + private readonly ILogger _logger; + private readonly ITestService _testService; + + public TestController(ILogger logger, + [KeyFilter("service")] ITestService testService) + { + this._logger = logger; + this._testService = testService; + } + + [HttpGet] + public IActionResult Get() + { + return this.Ok(this._testService.GetDate()); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/IMessager.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/IMessager.cs index 07685fa7..35d3c543 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/IMessager.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/IMessager.cs @@ -1,7 +1,7 @@ -namespace WebApiNetCore31 -{ - public interface IMessager - { - string OperationId { get; } - } +namespace WebApiNetCore31 +{ + public interface IMessager + { + string OperationId { get; } + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/IScopeMessager.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/IScopeMessager.cs index 101c08fa..c91f6890 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/IScopeMessager.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/IScopeMessager.cs @@ -1,6 +1,6 @@ -namespace WebApiNetCore31 -{ - public interface IScopeMessager : IMessager - { - } +namespace WebApiNetCore31 +{ + public interface IScopeMessager : IMessager + { + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/ISingleMessager.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/ISingleMessager.cs index e803b8c9..5d2f7fb1 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/ISingleMessager.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/ISingleMessager.cs @@ -1,6 +1,6 @@ -namespace WebApiNetCore31 -{ - public interface ISingleMessager : IMessager - { - } -} +namespace WebApiNetCore31 +{ + public interface ISingleMessager : IMessager + { + } +} diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/ITransientMessager.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/ITransientMessager.cs index b152a276..4238a3ee 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/ITransientMessager.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/ITransientMessager.cs @@ -1,6 +1,6 @@ -namespace WebApiNetCore31 -{ - public interface ITransientMessager : IMessager - { - } +namespace WebApiNetCore31 +{ + public interface ITransientMessager : IMessager + { + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/LogMessager.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/LogMessager.cs index 633adcba..f8725cea 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/LogMessager.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/LogMessager.cs @@ -1,9 +1,9 @@ -using System; - -namespace WebApiNetCore31 -{ - internal class LogMessager : IMessager - { - public string OperationId { get; } = $"日誌-{Guid.NewGuid()}"; - } +using System; + +namespace WebApiNetCore31 +{ + internal class LogMessager : IMessager + { + public string OperationId { get; } = $"日誌-{Guid.NewGuid()}"; + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/MachineMessager.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/MachineMessager.cs index d3e4038f..9feede22 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/MachineMessager.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/MachineMessager.cs @@ -1,9 +1,9 @@ -using System; - -namespace WebApiNetCore31 -{ - internal class MachineMessager : IMessager - { - public string OperationId { get; } = $"機器-{Guid.NewGuid()}"; - } +using System; + +namespace WebApiNetCore31 +{ + internal class MachineMessager : IMessager + { + public string OperationId { get; } = $"機器-{Guid.NewGuid()}"; + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/MultiMessager.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/MultiMessager.cs index e4eb78a9..673a9a2c 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/MultiMessager.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Message/MultiMessager.cs @@ -1,9 +1,9 @@ -using System; - -namespace WebApiNetCore31 -{ - public class MultiMessager : IScopeMessager, ISingleMessager, ITransientMessager - { - public string OperationId { get; } = $"多個接口-{Guid.NewGuid()}"; - } +using System; + +namespace WebApiNetCore31 +{ + public class MultiMessager : IScopeMessager, ISingleMessager, ITransientMessager + { + public string OperationId { get; } = $"多個接口-{Guid.NewGuid()}"; + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Program.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Program.cs index 9ff36d18..3d6598b4 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Program.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Program.cs @@ -1,22 +1,22 @@ -using Autofac.Extensions.DependencyInjection; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace WebApiNetCore31 -{ - public class Program - { - public static IHostBuilder CreateHostBuilder(string[] args) - { - return Host.CreateDefaultBuilder(args) - .UseServiceProviderFactory(new AutofacServiceProviderFactory()) - //.ConfigureServices(services => services.AddAutofac()) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); - } - - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - } +using Autofac.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace WebApiNetCore31 +{ + public class Program + { + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + //.ConfigureServices(services => services.AddAutofac()) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } + + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Properties/launchSettings.json b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Properties/launchSettings.json index 21810b86..b39adb37 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Properties/launchSettings.json +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Properties/launchSettings.json @@ -1,30 +1,30 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:54205", - "sslPort": 44308 - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "default", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "WebApiNetCore31": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" - } - } +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:54205", + "sslPort": 44308 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "default", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WebApiNetCore31": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Startup.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Startup.cs index 255e068b..0cc35cfc 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/Startup.cs +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/Startup.cs @@ -1,49 +1,49 @@ -using System.Reflection; -using Autofac; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace WebApiNetCore31 -{ - public class Startup - { - public IConfiguration Configuration { get; } - - public Startup(IConfiguration configuration) - { - this.Configuration = configuration; - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); - } - - public void ConfigureContainer(ContainerBuilder builder) - { - var assembly = Assembly.GetExecutingAssembly(); - builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces(); - } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - } - } +using System.Reflection; +using Autofac; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace WebApiNetCore31 +{ + public class Startup + { + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + { + this.Configuration = configuration; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + + public void ConfigureContainer(ContainerBuilder builder) + { + var assembly = Assembly.GetExecutingAssembly(); + builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces(); + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + } + } } \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/WebApiNetCore31.csproj b/DI/Lab.MsDIForAutofac/WebApiNetCore31/WebApiNetCore31.csproj index 3172ea45..93ae2103 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/WebApiNetCore31.csproj +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/WebApiNetCore31.csproj @@ -1,20 +1,20 @@ - - - - netcoreapp3.1 - - - - - - - - - - - - - - - - + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/WebApiNetCore31.csproj.user b/DI/Lab.MsDIForAutofac/WebApiNetCore31/WebApiNetCore31.csproj.user new file mode 100644 index 00000000..dc63f8a8 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/WebApiNetCore31.csproj.user @@ -0,0 +1,6 @@ + + + + false + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/a.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/a.cs new file mode 100644 index 00000000..c765a72d --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/a.cs @@ -0,0 +1,29 @@ +using System; + +namespace WebApiNetCore31 +{ + public interface IFileProvider + { + string Print(); + } + + public class FileProvider : IFileProvider + { + public string Print() + { + var msg = "FileProvider"; + Console.WriteLine(msg); + return msg; + } + } + + public class ZipFileProvider : IFileProvider + { + public string Print() + { + var msg = "ZipFileProvider"; + Console.WriteLine(msg); + return msg; + } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/appsettings.Development.json b/DI/Lab.MsDIForAutofac/WebApiNetCore31/appsettings.Development.json index 8983e0fc..dba68eb1 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/appsettings.Development.json +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/appsettings.Development.json @@ -1,9 +1,9 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" } } } diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/appsettings.json b/DI/Lab.MsDIForAutofac/WebApiNetCore31/appsettings.json index d9d9a9bf..81ff8777 100644 --- a/DI/Lab.MsDIForAutofac/WebApiNetCore31/appsettings.json +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/appsettings.json @@ -1,10 +1,10 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/b.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/b.cs new file mode 100644 index 00000000..6e1a86a2 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/b.cs @@ -0,0 +1,39 @@ +namespace WebApiNetCore31 +{ + public interface ITestService + { + string GetDate(); + } + + public class TestService : ITestService + { + public string GetDate() + { + return "service"; + } + } + + public class TestComponent : ITestService + { + public string GetDate() + { + return "component"; + } + } + + public interface IServiceProvider + { + public void setService(); + } + + // Client class + public class Client + { + private IServiceProvider _service; + + public Client(IServiceProvider service) + { + this._service = service; + } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.AssemblyInfo.cs b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.AssemblyInfo.cs new file mode 100644 index 00000000..1296b66c --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.AssemblyInfo.cs @@ -0,0 +1,23 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("WebApiNetCore31")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("WebApiNetCore31")] +[assembly: System.Reflection.AssemblyTitleAttribute("WebApiNetCore31")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Generated by the MSBuild WriteCodeFragment class. + diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.AssemblyInfoInputs.cache b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.AssemblyInfoInputs.cache new file mode 100644 index 00000000..b94c1aa8 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +563fea439ee8548554f13f24a19fba2d41da1919 diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.GeneratedMSBuildEditorConfig.editorconfig b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 00000000..f362fefd --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,3 @@ +is_global = true +build_property.RootNamespace = WebApiNetCore31 +build_property.ProjectDir = D:\src\sample.dotblog\DI\Lab.MsDIForAutofac\WebApiNetCore31\ diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.assets.cache b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.assets.cache new file mode 100644 index 00000000..6440b2a6 Binary files /dev/null and b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.assets.cache differ diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.csproj.AssemblyReference.cache b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.csproj.AssemblyReference.cache new file mode 100644 index 00000000..d5029fad Binary files /dev/null and b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/Debug/netcoreapp3.1/WebApiNetCore31.csproj.AssemblyReference.cache differ diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/WebApiNetCore31.csproj.nuget.dgspec.json b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/WebApiNetCore31.csproj.nuget.dgspec.json new file mode 100644 index 00000000..f1963013 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/WebApiNetCore31.csproj.nuget.dgspec.json @@ -0,0 +1,70 @@ +{ + "format": 1, + "restore": { + "D:\\src\\sample.dotblog\\DI\\Lab.MsDIForAutofac\\WebApiNetCore31\\WebApiNetCore31.csproj": {} + }, + "projects": { + "D:\\src\\sample.dotblog\\DI\\Lab.MsDIForAutofac\\WebApiNetCore31\\WebApiNetCore31.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "D:\\src\\sample.dotblog\\DI\\Lab.MsDIForAutofac\\WebApiNetCore31\\WebApiNetCore31.csproj", + "projectName": "WebApiNetCore31", + "projectPath": "D:\\src\\sample.dotblog\\DI\\Lab.MsDIForAutofac\\WebApiNetCore31\\WebApiNetCore31.csproj", + "packagesPath": "C:\\Users\\Yao Chang Yu\\.nuget\\packages\\", + "outputPath": "D:\\src\\sample.dotblog\\DI\\Lab.MsDIForAutofac\\WebApiNetCore31\\obj\\", + "projectStyle": "PackageReference", + "configFilePaths": [ + "C:\\Users\\Yao Chang Yu\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "netcoreapp3.1" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {} + }, + "frameworks": { + "netcoreapp3.1": { + "targetAlias": "netcoreapp3.1", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + } + }, + "frameworks": { + "netcoreapp3.1": { + "targetAlias": "netcoreapp3.1", + "dependencies": { + "Autofac.Extensions.DependencyInjection": { + "target": "Package", + "version": "[7.1.0, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.AspNetCore.App": { + "privateAssets": "none" + }, + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\5.0.400\\RuntimeIdentifierGraph.json" + } + } + } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/WebApiNetCore31.csproj.nuget.g.props b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/WebApiNetCore31.csproj.nuget.g.props new file mode 100644 index 00000000..ad359c0b --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/WebApiNetCore31.csproj.nuget.g.props @@ -0,0 +1,18 @@ + + + + False + NuGet + $(MSBuildThisFileDirectory)project.assets.json + $(UserProfile)\.nuget\packages\ + C:\Users\Yao Chang Yu\.nuget\packages\ + PackageReference + 5.11.0 + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/WebApiNetCore31.csproj.nuget.g.targets b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/WebApiNetCore31.csproj.nuget.g.targets new file mode 100644 index 00000000..d212750c --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/WebApiNetCore31.csproj.nuget.g.targets @@ -0,0 +1,6 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/project.assets.json b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/project.assets.json new file mode 100644 index 00000000..91d6469b --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/project.assets.json @@ -0,0 +1,88 @@ +{ + "version": 3, + "targets": { + ".NETCoreApp,Version=v3.1": {} + }, + "libraries": {}, + "projectFileDependencyGroups": { + ".NETCoreApp,Version=v3.1": [ + "Autofac.Extensions.DependencyInjection >= 7.1.0" + ] + }, + "packageFolders": { + "C:\\Users\\Yao Chang Yu\\.nuget\\packages\\": {} + }, + "project": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "D:\\src\\sample.dotblog\\DI\\Lab.MsDIForAutofac\\WebApiNetCore31\\WebApiNetCore31.csproj", + "projectName": "WebApiNetCore31", + "projectPath": "D:\\src\\sample.dotblog\\DI\\Lab.MsDIForAutofac\\WebApiNetCore31\\WebApiNetCore31.csproj", + "packagesPath": "C:\\Users\\Yao Chang Yu\\.nuget\\packages\\", + "outputPath": "D:\\src\\sample.dotblog\\DI\\Lab.MsDIForAutofac\\WebApiNetCore31\\obj\\", + "projectStyle": "PackageReference", + "configFilePaths": [ + "C:\\Users\\Yao Chang Yu\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "netcoreapp3.1" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {} + }, + "frameworks": { + "netcoreapp3.1": { + "targetAlias": "netcoreapp3.1", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + } + }, + "frameworks": { + "netcoreapp3.1": { + "targetAlias": "netcoreapp3.1", + "dependencies": { + "Autofac.Extensions.DependencyInjection": { + "target": "Package", + "version": "[7.1.0, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.AspNetCore.App": { + "privateAssets": "none" + }, + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\5.0.400\\RuntimeIdentifierGraph.json" + } + } + }, + "logs": [ + { + "code": "NU1101", + "level": "Error", + "message": "Unable to find package Autofac.Extensions.DependencyInjection. No packages exist with this id in source(s): Microsoft Visual Studio Offline Packages", + "libraryId": "Autofac.Extensions.DependencyInjection", + "targetGraphs": [ + ".NETCoreApp,Version=v3.1" + ] + } + ] +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/project.nuget.cache b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/project.nuget.cache new file mode 100644 index 00000000..f0606a64 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WebApiNetCore31/obj/project.nuget.cache @@ -0,0 +1,18 @@ +{ + "version": 2, + "dgSpecHash": "+oR7kmoX58hU75JnQMAzXJl+PfV8Klr4xZc4raw2Ndh3DuN5Ev+x0AdClXbVyIWS+ICIvv8tTLq6Y376zhkbMQ==", + "success": false, + "projectFilePath": "D:\\src\\sample.dotblog\\DI\\Lab.MsDIForAutofac\\WebApiNetCore31\\WebApiNetCore31.csproj", + "expectedPackageFiles": [], + "logs": [ + { + "code": "NU1101", + "level": "Error", + "message": "Unable to find package Autofac.Extensions.DependencyInjection. No packages exist with this id in source(s): Microsoft Visual Studio Offline Packages", + "libraryId": "Autofac.Extensions.DependencyInjection", + "targetGraphs": [ + ".NETCoreApp,Version=v3.1" + ] + } + ] +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/App.config b/DI/Lab.MsDIForAutofac/WinFormNet48/App.config new file mode 100644 index 00000000..3916e0e4 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/Form2.Designer.cs b/DI/Lab.MsDIForAutofac/WinFormNet48/Form2.Designer.cs new file mode 100644 index 00000000..18e39a8b --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/Form2.Designer.cs @@ -0,0 +1,59 @@ +namespace WinFormNet48 +{ + partial class Form2 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.button1 = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // button1 + // + this.button1.Location = new System.Drawing.Point(693, 394); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(75, 23); + this.button1.TabIndex = 0; + this.button1.Text = "button1"; + this.button1.UseVisualStyleBackColor = true; + // + // Form2 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.button1); + this.Name = "Form2"; + this.Text = "Form2"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Button button1; + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/Form2.cs b/DI/Lab.MsDIForAutofac/WinFormNet48/Form2.cs new file mode 100644 index 00000000..6222446f --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/Form2.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace WinFormNet48 +{ + public partial class Form2 : Form + { + public Form2() + { + InitializeComponent(); + } + } +} diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/Form2.resx b/DI/Lab.MsDIForAutofac/WinFormNet48/Form2.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/Form2.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/Form3.Designer.cs b/DI/Lab.MsDIForAutofac/WinFormNet48/Form3.Designer.cs new file mode 100644 index 00000000..e4bf0689 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/Form3.Designer.cs @@ -0,0 +1,39 @@ +namespace WinFormNet48 +{ + partial class Form3 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Text = "Form3"; + } + + #endregion + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/Form3.cs b/DI/Lab.MsDIForAutofac/WinFormNet48/Form3.cs new file mode 100644 index 00000000..f170710b --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/Form3.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace WinFormNet48 +{ + public partial class Form3 : Form + { + public Form3() + { + InitializeComponent(); + } + } +} diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/MainForm.Designer.cs b/DI/Lab.MsDIForAutofac/WinFormNet48/MainForm.Designer.cs new file mode 100644 index 00000000..fb1bb09c --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/MainForm.Designer.cs @@ -0,0 +1,95 @@ +namespace WinFormNet48 +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.splitContainer1 = new System.Windows.Forms.SplitContainer(); + this.button2 = new System.Windows.Forms.Button(); + this.button1 = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); + this.splitContainer1.Panel1.SuspendLayout(); + this.splitContainer1.SuspendLayout(); + this.SuspendLayout(); + // + // splitContainer1 + // + this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer1.Location = new System.Drawing.Point(0, 0); + this.splitContainer1.Name = "splitContainer1"; + // + // splitContainer1.Panel1 + // + this.splitContainer1.Panel1.Controls.Add(this.button2); + this.splitContainer1.Panel1.Controls.Add(this.button1); + this.splitContainer1.Size = new System.Drawing.Size(800, 450); + this.splitContainer1.SplitterDistance = 266; + this.splitContainer1.TabIndex = 0; + // + // button2 + // + this.button2.Location = new System.Drawing.Point(71, 78); + this.button2.Name = "button2"; + this.button2.Size = new System.Drawing.Size(75, 23); + this.button2.TabIndex = 1; + this.button2.Text = "button2"; + this.button2.UseVisualStyleBackColor = true; + this.button2.Click += new System.EventHandler(this.button2_Click); + // + // button1 + // + this.button1.Location = new System.Drawing.Point(71, 37); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(75, 23); + this.button1.TabIndex = 0; + this.button1.Text = "button1"; + this.button1.UseVisualStyleBackColor = true; + this.button1.Click += new System.EventHandler(this.button1_Click); + // + // MainForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.splitContainer1); + this.Name = "MainForm"; + this.Text = "Form1"; + this.splitContainer1.Panel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit(); + this.splitContainer1.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.SplitContainer splitContainer1; + private System.Windows.Forms.Button button2; + private System.Windows.Forms.Button button1; + } +} + diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/MainForm.cs b/DI/Lab.MsDIForAutofac/WinFormNet48/MainForm.cs new file mode 100644 index 00000000..c9ea9f2d --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/MainForm.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace WinFormNet48 +{ + public partial class MainForm : Form + { + private readonly Dictionary _subFormLook = new Dictionary(); + private Form _previousForm; + + public MainForm() + { + this.InitializeComponent(); + } + + private void button1_Click(object sender, EventArgs e) + { + this.Show("Form2"); + } + + private void button2_Click(object sender, EventArgs e) + { + this.Show("Form3"); + } + + private void Show(string name) + { + Form subForm; + if (this._subFormLook.ContainsKey(name) == false) + { + subForm = new Form2(); + subForm.TopLevel = true; + subForm.Visible = true; + subForm.WindowState = FormWindowState.Maximized; + subForm.Dock = DockStyle.Fill; + //subForm.ControlBox = false; + this._subFormLook.Add(name, subForm); + //this.splitContainer1.Panel2.Controls.Add(subForm); + } + + subForm = this._subFormLook[name]; + + subForm.Show(); + + //if (this._previousForm != null) + //{ + // if (this._previousForm.Name != subForm.Name) + // { + // this._previousForm.Hide(); + // } + //} + + //this._previousForm = subForm; + } + } +} \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/MainForm.resx b/DI/Lab.MsDIForAutofac/WinFormNet48/MainForm.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/MainForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/Program.cs b/DI/Lab.MsDIForAutofac/WinFormNet48/Program.cs new file mode 100644 index 00000000..149cd059 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace WinFormNet48 +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new MainForm()); + } + } +} diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/AssemblyInfo.cs b/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..5cabfbce --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WinFormNet48")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WinFormNet48")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("0fb52124-ec0f-4ba8-975e-8b3656dc0def")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Resources.Designer.cs b/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Resources.Designer.cs new file mode 100644 index 00000000..bf954d2b --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WinFormNet48.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WinFormNet48.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Resources.resx b/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Resources.resx new file mode 100644 index 00000000..ffecec85 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Settings.Designer.cs b/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Settings.Designer.cs new file mode 100644 index 00000000..f43f01d5 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WinFormNet48.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Settings.settings b/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Settings.settings new file mode 100644 index 00000000..abf36c5d --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/WinFormNet48.csproj b/DI/Lab.MsDIForAutofac/WinFormNet48/WinFormNet48.csproj new file mode 100644 index 00000000..21afa8cf --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/WinFormNet48.csproj @@ -0,0 +1,106 @@ + + + + + Debug + AnyCPU + {0FB52124-EC0F-4BA8-975E-8B3656DC0DEF} + WinExe + WinFormNet48 + WinFormNet48 + v4.8 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\linq2db.3.1.5\lib\net46\linq2db.dll + + + ..\packages\linq2db4iSeries.3.1.5\lib\net45\LinqToDB.DataProvider.DB2iSeries.dll + + + + + + + + + + + + + + + + + Form + + + MainForm.cs + + + Form + + + Form2.cs + + + Form + + + Form3.cs + + + + + Form2.cs + + + MainForm.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + \ No newline at end of file diff --git a/DI/Lab.MsDIForAutofac/WinFormNet48/packages.config b/DI/Lab.MsDIForAutofac/WinFormNet48/packages.config new file mode 100644 index 00000000..3636f3c7 --- /dev/null +++ b/DI/Lab.MsDIForAutofac/WinFormNet48/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Client/Client.csproj b/DI/Lab.MultipleImpl/Client/Client.csproj new file mode 100644 index 00000000..e7e23018 --- /dev/null +++ b/DI/Lab.MultipleImpl/Client/Client.csproj @@ -0,0 +1,23 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + + + + diff --git a/DI/Lab.MultipleImpl/Client/UnitTest1.cs b/DI/Lab.MultipleImpl/Client/UnitTest1.cs new file mode 100644 index 00000000..0dcc77b8 --- /dev/null +++ b/DI/Lab.MultipleImpl/Client/UnitTest1.cs @@ -0,0 +1,128 @@ +using System; +using Autofac; +using Autofac.Extensions.DependencyInjection; +using Autofac.Features.AttributeFilters; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Server; +using Server.Controllers; +using Unity; +using Unity.Microsoft.DependencyInjection; + +namespace Client +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void Autofac注入ServiceName() + { + var hostBuilder = WebHost.CreateDefaultBuilder() + // .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureServices(services => { services.AddAutofac(); }) + .UseStartup() + ; + using var server = new TestServer(hostBuilder) + { + BaseAddress = new Uri("http://localhost:9527") + }; + + var client = server.CreateClient(); + var url = "autofac"; + var response = client.GetAsync(url).Result; + response.EnsureSuccessStatusCode(); + + var result = response.Content.ReadAsStringAsync().Result; + Assert.AreEqual("ZipFileProvider", result); + } + + [TestMethod] + public void Unity注入ServiceName() + { + var unityContainer = new UnityContainer(); + ConfigureContainer(unityContainer); + + using var server = + new TestServer(WebHost.CreateDefaultBuilder() + .UseStartup() + .UseUnityServiceProvider(unityContainer) + .ConfigureServices(UseUnityController) + ) + { + BaseAddress = new Uri("http://localhost:9527") + }; + + var client = server.CreateClient(); + var url = "unity"; + var response = client.GetAsync(url).Result; + response.EnsureSuccessStatusCode(); + + var result = response.Content.ReadAsStringAsync().Result; + Assert.AreEqual("ZipFileProvider", result); + } + + [TestMethod] + public void 注入FuncName() + { + using var server = + new TestServer(WebHost.CreateDefaultBuilder() + .UseStartup() + .ConfigureServices(UseFuncName) + ) + { + BaseAddress = new Uri("http://localhost:9527") + }; + + var client = server.CreateClient(); + var url = "default/zip"; + var response = client.GetAsync(url).Result; + response.EnsureSuccessStatusCode(); + + var result = response.Content.ReadAsStringAsync().Result; + Assert.AreEqual("ZipFileProvider", result); + } + + private static void ConfigureContainer(ContainerBuilder builder) + { + // builder.RegisterType().Keyed("file"); + // builder.RegisterType().Keyed("zip"); + // builder.RegisterType().WithAttributeFiltering(); + } + + private static void ConfigureContainer(IUnityContainer container) + { + container.RegisterType("zip"); + container.RegisterType("file"); + } + + private static void UseFuncName(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton>(provider => + key => + { + switch (key) + { + case "zip": + return provider + .GetService(); + case "file": + return provider + .GetService(); + default: + throw new NotSupportedException(); + } + }); + } + + private static void UseUnityController(IServiceCollection services) + { + services.AddControllers() + .AddControllersAsServices(); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Lab.MultipleImpl.sln b/DI/Lab.MultipleImpl/Lab.MultipleImpl.sln new file mode 100644 index 00000000..8a158dd3 --- /dev/null +++ b/DI/Lab.MultipleImpl/Lab.MultipleImpl.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{DAE7F74D-E847-4B2F-8930-59AF2698FD1D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{6ADCD6A4-6733-46CF-8935-C2D77FC7FC79}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NET5.TestProject", "NET5.TestProject\NET5.TestProject.csproj", "{A433C8F8-3B75-412E-955F-287639C55C5F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DAE7F74D-E847-4B2F-8930-59AF2698FD1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DAE7F74D-E847-4B2F-8930-59AF2698FD1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DAE7F74D-E847-4B2F-8930-59AF2698FD1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DAE7F74D-E847-4B2F-8930-59AF2698FD1D}.Release|Any CPU.Build.0 = Release|Any CPU + {6ADCD6A4-6733-46CF-8935-C2D77FC7FC79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6ADCD6A4-6733-46CF-8935-C2D77FC7FC79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6ADCD6A4-6733-46CF-8935-C2D77FC7FC79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6ADCD6A4-6733-46CF-8935-C2D77FC7FC79}.Release|Any CPU.Build.0 = Release|Any CPU + {A433C8F8-3B75-412E-955F-287639C55C5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A433C8F8-3B75-412E-955F-287639C55C5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A433C8F8-3B75-412E-955F-287639C55C5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A433C8F8-3B75-412E-955F-287639C55C5F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/AutofacStartup.cs b/DI/Lab.MultipleImpl/NET5.TestProject/AutofacStartup.cs new file mode 100644 index 00000000..a1721c5a --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/AutofacStartup.cs @@ -0,0 +1,53 @@ +using Autofac; +using Autofac.Features.AttributeFilters; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NET5.TestProject.Controllers; +using NET5.TestProject.File; + +namespace NET5.TestProject +{ + public class AutofacStartup + { + public AutofacStartup(IConfiguration configuration) + { + this.Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureContainer(ContainerBuilder builder) + { + builder.RegisterType().Keyed("file"); + builder.RegisterType().Keyed("zip"); + builder.RegisterType().WithAttributeFiltering();//<-- add line + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddControllersAsServices(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/AutofacController.cs b/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/AutofacController.cs new file mode 100644 index 00000000..50bdf475 --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/AutofacController.cs @@ -0,0 +1,38 @@ +using System; +using Autofac; +using Autofac.Extensions.DependencyInjection; +using Autofac.Features.AttributeFilters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using NET5.TestProject.File; + +namespace NET5.TestProject.Controllers +{ + [ApiController] + [Route("[controller]")] + public class AutofacController : ControllerBase + { + private readonly IFileProvider _fileProvider; + + private readonly ILogger _logger; + + public AutofacController(ILogger logger, + [KeyFilter("zip")] IFileProvider fileProvider) + { + this._logger = logger; + this._fileProvider = fileProvider; + var msg = $"{this._fileProvider.Print()} in {this.GetType().Name} constructor"; + Console.WriteLine(msg); + } + + [HttpGet] + [Route("{key}")] + public IActionResult Get(string key) + { + var serviceProvider = this.HttpContext.RequestServices; + var autofacServiceProvider = (AutofacServiceProvider) serviceProvider; + var fileProvider = autofacServiceProvider.LifetimeScope.ResolveKeyed(key); + return this.Ok(fileProvider.Print()); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/DefaultController.cs b/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/DefaultController.cs new file mode 100644 index 00000000..fad0093e --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/DefaultController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NET5.TestProject.File; + +namespace NET5.TestProject.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DefaultController : ControllerBase + { + private readonly ILogger _logger; + private readonly IFileProvider _fileProvider; + + public DefaultController(ILogger logger, + IFileProvider fileProvider) + { + this._logger = logger; + this._fileProvider = fileProvider; + } + [HttpGet] + public IActionResult Get() + { + // var fileProvider = this.HttpContext.RequestServices.GetService(); + var fileProvider = this._fileProvider; + var result = fileProvider.Print(); + return this.Ok(result); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/FuncController.cs b/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/FuncController.cs new file mode 100644 index 00000000..43728f63 --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/FuncController.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using NET5.TestProject.File; + +namespace NET5.TestProject.Controllers +{ + [ApiController] + [Route("[controller]")] + public class FuncController : ControllerBase + { + private readonly IFileProvider _fileProvider; + private readonly ILogger _logger; + + public FuncController(ILogger logger, + Func pool) + { + this._fileProvider = pool("zip"); + this._logger = logger; + var msg = $"{this._fileProvider.Print()} in {this.GetType().Name} constructor"; + Console.WriteLine(msg); + } + + [HttpGet] + [Route("{type}")] + public IActionResult Get(string type) + { + var fileProvider = this.HttpContext.RequestServices.GetService(type); + var result = fileProvider.Print(); + return this.Ok(result); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/MultiController.cs b/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/MultiController.cs new file mode 100644 index 00000000..48942f3f --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/MultiController.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NET5.TestProject.File; + +namespace NET5.TestProject.Controllers +{ + [ApiController] + [Route("[controller]")] + public class MultiController : ControllerBase + { + private readonly IFileProvider _fileProvider; + + private readonly ILogger _logger; + + // public MultiController(ILogger logger, + // IEnumerable pool) + // { + // this._logger = logger; + // this._fileProvider = pool.FirstOrDefault(p => p.GetType().Name == "ZipFileProvider"); + // var msg = $"{this._fileProvider.Print()} in {this.GetType().Name} constructor"; + // Console.WriteLine(msg); + // } + // + // [HttpGet] + // public IActionResult Get() + // { + // var serviceProvider = this.HttpContext.RequestServices; + // var pool = serviceProvider.GetServices(); + // var fileProvider = pool.FirstOrDefault(p => p.GetType().Name == "ZipFileProvider"); + // return this.Ok(fileProvider.Print()); + // } + + public MultiController(ILogger logger, + Dictionary pool) + { + this._logger = logger; + this._fileProvider = pool["zip"]; + var msg = $"{this._fileProvider.Print()} in {this.GetType().Name} constructor"; + Console.WriteLine(msg); + } + + [HttpGet] + [Route("{key}")] + public IActionResult Get(string key) + { + var serviceProvider = this.HttpContext.RequestServices; + var pool = serviceProvider.GetService>(); + var fileProvider = pool[key]; + return this.Ok(fileProvider.Print()); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/UnityController.cs b/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/UnityController.cs new file mode 100644 index 00000000..c94576a0 --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/Controllers/UnityController.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using NET5.TestProject.File; +using Unity; +using Unity.Microsoft.DependencyInjection; + +namespace NET5.TestProject.Controllers +{ + [ApiController] + [Route("[controller]")] + public class UnityController : ControllerBase + { + private readonly IFileProvider _fileProvider; + + private readonly ILogger _logger; + + public UnityController(ILogger logger, + [Dependency("zip")] IFileProvider fileProvider) + { + this._logger = logger; + this._fileProvider = fileProvider; + var msg = $"{this._fileProvider.Print()} in {this.GetType().Name} constructor"; + Console.WriteLine(msg); + } + + [HttpGet] + [Route("{key}")] + public IActionResult Get(string key) + { + var serviceProvider = this.HttpContext.RequestServices; + var unityServiceProvider = (ServiceProvider) serviceProvider; + var unityContainer = (UnityContainer) unityServiceProvider; + var fileProvider = unityContainer.Resolve(key); + var result = fileProvider.Print(); + return this.Ok(result); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/File/FileProvider.cs b/DI/Lab.MultipleImpl/NET5.TestProject/File/FileProvider.cs new file mode 100644 index 00000000..f9ea7bea --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/File/FileProvider.cs @@ -0,0 +1,14 @@ +using System; + +namespace NET5.TestProject.File +{ + public class FileProvider : IFileProvider + { + public string Print() + { + var msg = "FileProvider"; + Console.WriteLine(msg); + return msg; + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/File/IFileProvider.cs b/DI/Lab.MultipleImpl/NET5.TestProject/File/IFileProvider.cs new file mode 100644 index 00000000..e72e465a --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/File/IFileProvider.cs @@ -0,0 +1,7 @@ +namespace NET5.TestProject.File +{ + public interface IFileProvider + { + string Print(); + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/File/ZipFileProvider.cs b/DI/Lab.MultipleImpl/NET5.TestProject/File/ZipFileProvider.cs new file mode 100644 index 00000000..058d383d --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/File/ZipFileProvider.cs @@ -0,0 +1,14 @@ +using System; + +namespace NET5.TestProject.File +{ + public class ZipFileProvider : IFileProvider + { + public string Print() + { + var msg = "ZipFileProvider"; + Console.WriteLine(msg); + return msg; + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/FileAdapter.cs b/DI/Lab.MultipleImpl/NET5.TestProject/FileAdapter.cs new file mode 100644 index 00000000..41034a00 --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/FileAdapter.cs @@ -0,0 +1,19 @@ +using NET5.TestProject.File; + +namespace NET5.TestProject +{ + public class FileAdapter + { + private readonly IFileProvider _fileProvider; + + public FileAdapter(IFileProvider fileProvider) + { + this._fileProvider = fileProvider; + } + + public string Get() + { + return this._fileProvider.Print(); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/FuncStartup.cs b/DI/Lab.MultipleImpl/NET5.TestProject/FuncStartup.cs new file mode 100644 index 00000000..5be65458 --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/FuncStartup.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NET5.TestProject.File; + +namespace NET5.TestProject +{ + public class FuncStartup + { + public FuncStartup(IConfiguration configuration) + { + this.Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + UseFuncName(services); + } + private static void UseFuncName(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton>(provider => + key => + { + switch (key) + { + case "zip": + return provider + .GetService(); + case "file": + return provider + .GetService(); + default: + throw new NotSupportedException(); + } + }); + } + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/NET5.TestProject.csproj b/DI/Lab.MultipleImpl/NET5.TestProject/NET5.TestProject.csproj new file mode 100644 index 00000000..1c284244 --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/NET5.TestProject.csproj @@ -0,0 +1,28 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + + + true + PreserveNewest + PreserveNewest + + + + diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/ServiceProviderExtension.cs b/DI/Lab.MultipleImpl/NET5.TestProject/ServiceProviderExtension.cs new file mode 100644 index 00000000..2c381a2f --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/ServiceProviderExtension.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using NET5.TestProject.File; + +namespace NET5.TestProject +{ + public static class ServiceProviderExtension + { + public static T GetService(this IServiceProvider provider, string name) + { + var pool = (Func) provider.GetService(typeof(Func)); + return (T) pool(name); + } + + public static List GetTypesAssignableFrom(this Assembly assembly) + { + return assembly.GetTypesAssignableFrom(typeof(T)); + } + + public static List GetTypesAssignableFrom(this Assembly assembly, Type compareType) + { + var results = new List(); + foreach (var type in assembly.DefinedTypes) + { + if (compareType.IsAssignableFrom(type) + && compareType != type + ) + { + results.Add(type); + } + } + + return results; + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/Startup.cs b/DI/Lab.MultipleImpl/NET5.TestProject/Startup.cs new file mode 100644 index 00000000..2bad24fc --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/Startup.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace NET5.TestProject +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + this.Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/UnitTest1.cs b/DI/Lab.MultipleImpl/NET5.TestProject/UnitTest1.cs new file mode 100644 index 00000000..6641e17a --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/UnitTest1.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Autofac.Extensions.DependencyInjection; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NET5.TestProject.Controllers; +using NET5.TestProject.File; +using Unity; +using Unity.Microsoft.DependencyInjection; + +namespace NET5.TestProject +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void Autofac注入ServiceName() + { + var hostBuilder = WebHost.CreateDefaultBuilder() + + // .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .UseStartup() //<-- add line + .ConfigureServices(services => + { + services.AddAutofac(); + services.AddControllers() + .AddControllersAsServices(); //<-- add line + }) + ; + using var server = new TestServer(hostBuilder) + { + BaseAddress = new Uri("http://localhost:9527") + }; + + var client = server.CreateClient(); + var url = "autofac/zip"; + var response = client.GetAsync(url).Result; + response.EnsureSuccessStatusCode(); + + var result = response.Content.ReadAsStringAsync().Result; + Assert.AreEqual("ZipFileProvider", result); + } + + [TestMethod] + public void Unity注入ServiceName() + { + var unityContainer = new UnityContainer(); + unityContainer.RegisterType("zip"); + unityContainer.RegisterType("file"); //<-- add line + + var builder = WebHost.CreateDefaultBuilder() + .UseStartup() + .UseUnityServiceProvider(unityContainer) //<-- add line + .ConfigureServices(s => + { + s.AddControllers() + .AddControllersAsServices(); //<-- add line + }) + ; + using var server = new TestServer(builder) + { + BaseAddress = new Uri("http://localhost:9527") + }; + + var client = server.CreateClient(); + var url = "unity/zip"; + var response = client.GetAsync(url).Result; + response.EnsureSuccessStatusCode(); + + var result = response.Content.ReadAsStringAsync().Result; + Assert.AreEqual("ZipFileProvider", result); + } + + [TestMethod] + public void 手動註冊() + { + var hostBuilder = + WebHost.CreateDefaultBuilder() + .UseStartup() + .ConfigureServices(s => + { + s.AddSingleton(); + s.AddSingleton(); + s.AddSingleton(p => + { + var fileProvider = p.GetService(); + var logger = + p.GetService>(); + return new DefaultController(logger, fileProvider); + }); + s.AddControllers().AddControllersAsServices(); + }) + ; + using var server = new TestServer(hostBuilder) + { + BaseAddress = new Uri("http://localhost:9527") + }; + + var client = server.CreateClient(); + var url = "default"; + var response = client.GetAsync(url).Result; + response.EnsureSuccessStatusCode(); + + var result = response.Content.ReadAsStringAsync().Result; + Assert.AreEqual("ZipFileProvider", result); + } + + [TestMethod] + public void 注入FuncName() + { + var builder = WebHost.CreateDefaultBuilder() + .UseStartup() + .ConfigureServices(s => + { + s.AddSingleton(); + s.AddSingleton(); + s.AddSingleton>(p => + key => + { + switch (key) + { + case "zip": + return p + .GetService(); + case "file": + return p + .GetService(); + default: + throw new NotSupportedException(); + } + }); + }) + ; + using var server = new TestServer(builder) + { + BaseAddress = new Uri("http://localhost:9527") + }; + + var client = server.CreateClient(); + var url = "func/zip"; + var response = client.GetAsync(url).Result; + response.EnsureSuccessStatusCode(); + + var result = response.Content.ReadAsStringAsync().Result; + Assert.AreEqual("ZipFileProvider", result); + } + + [TestMethod] + public void 注入相同的介面() + { + var hostBuilder = WebHost.CreateDefaultBuilder() + .UseStartup() //<-- add line + .ConfigureServices(service => + { + ScanToDictionary(service); + + // AddToDictionary(service); + }) + ; + using var server = new TestServer(hostBuilder) + { + BaseAddress = new Uri("http://localhost:9527") + }; + + var client = server.CreateClient(); + var url = "multi/zip"; + var response = client.GetAsync(url).Result; + response.EnsureSuccessStatusCode(); + + var result = response.Content.ReadAsStringAsync().Result; + Assert.AreEqual("ZipFileProvider", result); + } + + private static void AddToDictionary(IServiceCollection s) + { + s.AddSingleton(); + s.AddSingleton(); + s.AddSingleton(p => + { + var pool = + new Dictionary + { + {"zip", p.GetService()}, + {"file", p.GetService()} + }; + + return pool; + }); + } + + private static void ScanToDictionary(IServiceCollection services) + { + var assembly = Assembly.GetExecutingAssembly(); + assembly.GetTypesAssignableFrom() + .ForEach(t => { services.AddSingleton(t); }); + services.AddSingleton(p => + { + var pool = + new Dictionary + { + {"zip", p.GetService()}, + {"file", p.GetService()} + }; + + return pool; + }); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/NET5.TestProject/appsettings.json b/DI/Lab.MultipleImpl/NET5.TestProject/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/DI/Lab.MultipleImpl/NET5.TestProject/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/DI/Lab.MultipleImpl/Server/AutofacStartup.cs b/DI/Lab.MultipleImpl/Server/AutofacStartup.cs new file mode 100644 index 00000000..1e93de1e --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/AutofacStartup.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Autofac; +using Autofac.Features.AttributeFilters; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; +using Server.Controllers; + +namespace Server +{ + public class AutofacStartup + { + public AutofacStartup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureContainer(ContainerBuilder builder) + { + builder.RegisterType().Keyed("file"); + builder.RegisterType().Keyed("zip"); + builder.RegisterType().WithAttributeFiltering(); + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddControllersAsServices(); + services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo {Title = "Server", Version = "v1"}); }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Server v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/Controllers/AutofacController.cs b/DI/Lab.MultipleImpl/Server/Controllers/AutofacController.cs new file mode 100644 index 00000000..45debe20 --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/Controllers/AutofacController.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using Autofac.Features.AttributeFilters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Server.Controllers +{ + [ApiController] + [Route("[controller]")] + public class AutofacController : ControllerBase + { + private readonly IFileProvider _fileProvider; + + private readonly ILogger _logger; + + // public AutofacDefaultController(ILogger logger) + // { + // this._logger = logger; + // } + + public AutofacController(ILogger logger, + [KeyFilter("zip")] IFileProvider fileProvider) + { + this._logger = logger; + this._fileProvider = fileProvider; + this._fileProvider.Print(); + } + + [HttpGet] + public IActionResult Get() + { + return this.Ok(this._fileProvider.Print()); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/Controllers/DefaultController.cs b/DI/Lab.MultipleImpl/Server/Controllers/DefaultController.cs new file mode 100644 index 00000000..79efc330 --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/Controllers/DefaultController.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Server.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DefaultController : ControllerBase + { + + private readonly ILogger _logger; + + public DefaultController(ILogger logger) + { + _logger = logger; + } + + [HttpGet] + [Route("{type}")] + public IActionResult Get(string type) + { + var fileProvider = this.HttpContext.RequestServices.GetService(type); + var result = fileProvider.Print(); + return this.Ok(result); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/Controllers/UnityController.cs b/DI/Lab.MultipleImpl/Server/Controllers/UnityController.cs new file mode 100644 index 00000000..44910a17 --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/Controllers/UnityController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Unity; + +namespace Server.Controllers +{ + [ApiController] + [Route("[controller]")] + public class UnityController : ControllerBase + { + private readonly IFileProvider _fileProvider; + + private readonly ILogger _logger; + + public UnityController(ILogger logger, + [Dependency("zip")] IFileProvider fileProvider) + { + this._logger = logger; + this._fileProvider = fileProvider; + } + + [HttpGet] + public IActionResult Get() + { + var serviceProvider = this.HttpContext.RequestServices; + var unityServiceProvider = (Unity.Microsoft.DependencyInjection.ServiceProvider) serviceProvider; + var unityContainer = (UnityContainer) unityServiceProvider; + var fileProvider = unityContainer.Resolve("zip"); + var result = fileProvider.Print(); + return this.Ok(result); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/Controllers/WeatherForecastController.cs b/DI/Lab.MultipleImpl/Server/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..69c4d675 --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/Controllers/WeatherForecastController.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Server.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet] + public IEnumerable Get() + { + var rng = new Random(); + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateTime.Now.AddDays(index), + TemperatureC = rng.Next(-20, 55), + Summary = Summaries[rng.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/DependencyConfig.cs b/DI/Lab.MultipleImpl/Server/DependencyConfig.cs new file mode 100644 index 00000000..b6df11a4 --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/DependencyConfig.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; + +namespace Server +{ + public class DependencyConfig + { + public static void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddControllersAsServices() + ; + } + public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Server v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/File/FileProvider.cs b/DI/Lab.MultipleImpl/Server/File/FileProvider.cs new file mode 100644 index 00000000..06045138 --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/File/FileProvider.cs @@ -0,0 +1,14 @@ +using System; + +namespace Server +{ + public class FileProvider : IFileProvider + { + public string Print() + { + var msg = "FileProvider"; + Console.WriteLine(msg); + return msg; + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/File/IFileProvider.cs b/DI/Lab.MultipleImpl/Server/File/IFileProvider.cs new file mode 100644 index 00000000..7c69a1ae --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/File/IFileProvider.cs @@ -0,0 +1,7 @@ +namespace Server +{ + public interface IFileProvider + { + string Print(); + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/File/ZipFileProvider.cs b/DI/Lab.MultipleImpl/Server/File/ZipFileProvider.cs new file mode 100644 index 00000000..804a286a --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/File/ZipFileProvider.cs @@ -0,0 +1,14 @@ +using System; + +namespace Server +{ + public class ZipFileProvider : IFileProvider + { + public string Print() + { + var msg = "ZipFileProvider"; + Console.WriteLine(msg); + return msg; + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/Program.cs b/DI/Lab.MultipleImpl/Server/Program.cs new file mode 100644 index 00000000..c083a4aa --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Autofac.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Server +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/Properties/launchSettings.json b/DI/Lab.MultipleImpl/Server/Properties/launchSettings.json new file mode 100644 index 00000000..f822a28b --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:59369", + "sslPort": 44389 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Server": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/DI/Lab.MultipleImpl/Server/Server.csproj b/DI/Lab.MultipleImpl/Server/Server.csproj new file mode 100644 index 00000000..153ed02c --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/Server.csproj @@ -0,0 +1,21 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + diff --git a/DI/Lab.MultipleImpl/Server/Server.csproj.DotSettings b/DI/Lab.MultipleImpl/Server/Server.csproj.DotSettings new file mode 100644 index 00000000..a9923e32 --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/Server.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/ServiceProviderExtension.cs b/DI/Lab.MultipleImpl/Server/ServiceProviderExtension.cs new file mode 100644 index 00000000..601138e2 --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/ServiceProviderExtension.cs @@ -0,0 +1,14 @@ +using System; + +namespace Server +{ + public static class ServiceProviderExtension + { + public static T GetService(this IServiceProvider provider, string name) + { + var pool = (Func) provider.GetService(typeof(Func)); + return (T) pool(name); + } + } + +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/Startup.cs b/DI/Lab.MultipleImpl/Server/Startup.cs new file mode 100644 index 00000000..e7ecb450 --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/Startup.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; + +namespace Server +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo {Title = "Server", Version = "v1"}); }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Server v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/WeatherForecast.cs b/DI/Lab.MultipleImpl/Server/WeatherForecast.cs new file mode 100644 index 00000000..36e011e2 --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/WeatherForecast.cs @@ -0,0 +1,15 @@ +using System; + +namespace Server +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int) (TemperatureC / 0.5556); + + public string Summary { get; set; } + } +} \ No newline at end of file diff --git a/DI/Lab.MultipleImpl/Server/appsettings.Development.json b/DI/Lab.MultipleImpl/Server/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/DI/Lab.MultipleImpl/Server/appsettings.json b/DI/Lab.MultipleImpl/Server/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/DI/Lab.MultipleImpl/Server/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/DI/Lib.MsDiForScrutor b/DI/Lib.MsDiForScrutor deleted file mode 160000 index f36a5e15..00000000 --- a/DI/Lib.MsDiForScrutor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f36a5e15db26213b8acfd0dcb823cf530996e1be diff --git a/DI/Lib.MsDiForScrutor/Lib.MsDiForScrutor.sln b/DI/Lib.MsDiForScrutor/Lib.MsDiForScrutor.sln new file mode 100644 index 00000000..d7ce1c92 --- /dev/null +++ b/DI/Lib.MsDiForScrutor/Lib.MsDiForScrutor.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30503.244 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiNetCore31", "WebApiNetCore31\WebApiNetCore31.csproj", "{7CBCB1D6-6FF5-44A0-A873-C0DCFD6630B8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7CBCB1D6-6FF5-44A0-A873-C0DCFD6630B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CBCB1D6-6FF5-44A0-A873-C0DCFD6630B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CBCB1D6-6FF5-44A0-A873-C0DCFD6630B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CBCB1D6-6FF5-44A0-A873-C0DCFD6630B8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D1F4D562-D1B5-4DFA-A971-5E7A1FA0E73F} + EndGlobalSection +EndGlobal diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Controllers/Default1Controller.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Controllers/Default1Controller.cs new file mode 100644 index 00000000..f639c8fe --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Controllers/Default1Controller.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace WebApiNetCore31.Controllers +{ + [ApiController] + [Route("[controller]")] + public class Default1Controller : ControllerBase + { + private IMessager Messager { get; } + + private readonly ILogger _logger; + //public Default1Controller(IMessager messager) + //{ + // this.Messager = messager; + //} + + public Default1Controller(ILogger logger, + IMessager messager + ) + { + this._logger = logger; + this.Messager = messager; + } + + + + [HttpGet] + public IActionResult Get() + { + var content = $"Messager:{this.Messager.OperationId}"; + this._logger.LogInformation("Messager:{message}", content); + return this.Ok(content); + } + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Controllers/DefaultController.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Controllers/DefaultController.cs new file mode 100644 index 00000000..c6f658ba --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Controllers/DefaultController.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace WebApiNetCore31.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DefaultController : ControllerBase + { + private IMessager Transient { get; } + + private IMessager Scope { get; } + + private IMessager Single { get; } + + private readonly ILogger _logger; + + public DefaultController(ILogger logger, + ITransientMessager transient, + IScopeMessager scope, + ISingleMessager single) + { + this._logger = logger; + + this.Transient = transient; + this.Scope = scope; + this.Single = single; + } + + [HttpGet] + public IActionResult Get() + { + var content = $"transient:{this.Transient.OperationId}\r\n" + + $"scope:{this.Scope.OperationId}\r\n" + + $"single:{this.Single.OperationId}"; + this._logger.LogInformation("transient = {transient},scope = {scope},single = {single}", + this.Transient.OperationId, + this.Scope.OperationId, + this.Single.OperationId); + return this.Ok(content); + } + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/IMessager.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/IMessager.cs new file mode 100644 index 00000000..07685fa7 --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/IMessager.cs @@ -0,0 +1,7 @@ +namespace WebApiNetCore31 +{ + public interface IMessager + { + string OperationId { get; } + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/IScopeMessager.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/IScopeMessager.cs new file mode 100644 index 00000000..101c08fa --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/IScopeMessager.cs @@ -0,0 +1,6 @@ +namespace WebApiNetCore31 +{ + public interface IScopeMessager : IMessager + { + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/ISingleMessager.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/ISingleMessager.cs new file mode 100644 index 00000000..e803b8c9 --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/ISingleMessager.cs @@ -0,0 +1,6 @@ +namespace WebApiNetCore31 +{ + public interface ISingleMessager : IMessager + { + } +} diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/ITransientMessager.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/ITransientMessager.cs new file mode 100644 index 00000000..b152a276 --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/ITransientMessager.cs @@ -0,0 +1,6 @@ +namespace WebApiNetCore31 +{ + public interface ITransientMessager : IMessager + { + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/LogMessager.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/LogMessager.cs new file mode 100644 index 00000000..633adcba --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/LogMessager.cs @@ -0,0 +1,9 @@ +using System; + +namespace WebApiNetCore31 +{ + internal class LogMessager : IMessager + { + public string OperationId { get; } = $"日誌-{Guid.NewGuid()}"; + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/MachineMessager.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/MachineMessager.cs new file mode 100644 index 00000000..d3e4038f --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/MachineMessager.cs @@ -0,0 +1,9 @@ +using System; + +namespace WebApiNetCore31 +{ + internal class MachineMessager : IMessager + { + public string OperationId { get; } = $"機器-{Guid.NewGuid()}"; + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/Messager.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/Messager.cs new file mode 100644 index 00000000..921613a7 --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/Messager.cs @@ -0,0 +1,9 @@ +using System; + +namespace WebApiNetCore31 +{ + public class Messager : IMessager + { + public string OperationId { get; } = $"訊息-{Guid.NewGuid()}"; + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/MultiMessager.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/MultiMessager.cs new file mode 100644 index 00000000..e4eb78a9 --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Message/MultiMessager.cs @@ -0,0 +1,9 @@ +using System; + +namespace WebApiNetCore31 +{ + public class MultiMessager : IScopeMessager, ISingleMessager, ITransientMessager + { + public string OperationId { get; } = $"多個接口-{Guid.NewGuid()}"; + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Program.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Program.cs new file mode 100644 index 00000000..aba702a0 --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace WebApiNetCore31 +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Properties/launchSettings.json b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Properties/launchSettings.json new file mode 100644 index 00000000..139d8acd --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51385", + "sslPort": 44396 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "default1", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WebApiNetCore31": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Startup.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Startup.cs new file mode 100644 index 00000000..71a2aa39 --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Startup.cs @@ -0,0 +1,72 @@ +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace WebApiNetCore31 +{ + public class Startup + { + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + { + this.Configuration = configuration; + } + + public void AutoConfigureServices(IServiceCollection services) + { + var assembly = Assembly.GetExecutingAssembly(); + + services.Scan(scan => scan.FromAssemblies(assembly) + .AddClasses(classes => classes.AssignableTo()) + .AsImplementedInterfaces() + .WithScopedLifetime() + ); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + //AutoConfigureServices(services); + this.CustomConfigureServices(services); + } + + public void CustomConfigureServices(IServiceCollection services) + { + var assembly = Assembly.GetExecutingAssembly(); + var filterTypes = from type in assembly.GetTypes() + where type.IsAbstract == false + where typeof(IMessager).IsAssignableFrom(type) + //where type.Name.EndsWith("Messsage") + select type; + + foreach (var type in filterTypes) + { + services.AddTransient( type); + } + } + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/WeatherForecast.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/WeatherForecast.cs new file mode 100644 index 00000000..1bb9e27c --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/WeatherForecast.cs @@ -0,0 +1,15 @@ +using System; + +namespace WebApiNetCore31 +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string Summary { get; set; } + } +} diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/WebApiNetCore31.csproj b/DI/Lib.MsDiForScrutor/WebApiNetCore31/WebApiNetCore31.csproj new file mode 100644 index 00000000..71dcbf40 --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/WebApiNetCore31.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/Worker.cs b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Worker.cs new file mode 100644 index 00000000..f069f7e4 --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/Worker.cs @@ -0,0 +1,22 @@ +namespace WebApiNetCore31 +{ + public class Worker + { + public IMessager Messager { get; set; } + + public Worker(IMessager messager) + { + this.Messager = messager; + } + } + + public class Worker2 + { + public IMessager Messager { get; set; } + + public Worker2(IMessager messager) + { + this.Messager = messager; + } + } +} \ No newline at end of file diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/appsettings.Development.json b/DI/Lib.MsDiForScrutor/WebApiNetCore31/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/DI/Lib.MsDiForScrutor/WebApiNetCore31/appsettings.json b/DI/Lib.MsDiForScrutor/WebApiNetCore31/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/DI/Lib.MsDiForScrutor/WebApiNetCore31/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.SurveyTest/Lab.DynamoDB.SurveyTest.csproj b/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.SurveyTest/Lab.DynamoDB.SurveyTest.csproj new file mode 100644 index 00000000..efa12ca2 --- /dev/null +++ b/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.SurveyTest/Lab.DynamoDB.SurveyTest.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + enable + + false + + + + + + + + + + + diff --git a/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.SurveyTest/UnitTest1.cs b/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.SurveyTest/UnitTest1.cs new file mode 100644 index 00000000..55041dc2 --- /dev/null +++ b/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.SurveyTest/UnitTest1.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void Survey_CreateTable() + { + var client = CreateAmazonDynamoDbClient(); + var request = new CreateTableRequest + { + AttributeDefinitions = new List() + { + new AttributeDefinition + { + AttributeName = "Id", + AttributeType = "N" + }, + new AttributeDefinition + { + AttributeName = "DateTime", + AttributeType = "S" + } + }, + + KeySchema = new List + { + new KeySchemaElement + { + AttributeName = "Id", + KeyType = "HASH" //Partition key + }, + new KeySchemaElement + { + AttributeName = "DateTime", + KeyType = "RANGE" //Range key + } + } + }; + var response = client.CreateTableAsync(request).Result; + } + + private static void CreateExampleTable(AmazonDynamoDBClient client, + string tableName, + CancellationToken cancel) + { + Console.WriteLine("\n*** Creating table ***"); + var request = new CreateTableRequest + { + AttributeDefinitions = new List() + { + new AttributeDefinition + { + AttributeName = "Id", + AttributeType = "N" + }, + new AttributeDefinition + { + AttributeName = "ReplyDateTime", + AttributeType = "N" + } + }, + KeySchema = new List + { + new KeySchemaElement + { + AttributeName = "Id", + KeyType = "HASH" //Partition key + }, + new KeySchemaElement + { + AttributeName = "ReplyDateTime", + KeyType = "RANGE" //Sort key + } + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 5, + WriteCapacityUnits = 6 + }, + TableName = tableName + }; + + var response = client.CreateTableAsync(request, cancel).Result; + + var tableDescription = response.TableDescription; + Console.WriteLine("{1}: {0} \t ReadsPerSec: {2} \t WritesPerSec: {3}", + tableDescription.TableStatus, + tableDescription.TableName, + tableDescription.ProvisionedThroughput.ReadCapacityUnits, + tableDescription.ProvisionedThroughput.WriteCapacityUnits); + + string status = tableDescription.TableStatus; + Console.WriteLine(tableName + " - " + status); + + WaitUntilTableReady(client, tableName,cancel); + } + private static void WaitUntilTableReady(AmazonDynamoDBClient client, string tableName,CancellationToken cancel) + { + string status = null; + // Let us wait until table is created. Call DescribeTable. + do + { + System.Threading.Thread.Sleep(5000); // Wait 5 seconds. + try + { + var res = client.DescribeTableAsync(new DescribeTableRequest + { + TableName = tableName + }, cancel).Result; + + Console.WriteLine("Table name: {0}, status: {1}", + res.Table.TableName, + res.Table.TableStatus); + status = res.Table.TableStatus; + } + catch (ResourceNotFoundException) + { + // DescribeTable is eventually consistent. So you might + // get resource not found. So we handle the potential exception. + } + } while (status != "ACTIVE"); + } + private static AmazonDynamoDBClient? CreateAmazonDynamoDbClient() + { + var clientConfig = new AmazonDynamoDBConfig + { + ServiceURL = "http://localhost:8000" + }; + var client = new AmazonDynamoDBClient(clientConfig); + return client; + } +} \ No newline at end of file diff --git a/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.SurveyTest/appsettings.json b/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.SurveyTest/appsettings.json new file mode 100644 index 00000000..31ebdc71 --- /dev/null +++ b/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.SurveyTest/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "DynamoDb": { + "LocalMode": true, + "LocalServiceUrl": "http://localhost:8000" + } +} \ No newline at end of file diff --git a/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.sln b/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.sln new file mode 100644 index 00000000..e63b587d --- /dev/null +++ b/DynamoDB/Lab.DynamoDB/Lab.DynamoDB.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.DynamoDB.SurveyTest", "Lab.DynamoDB.SurveyTest\Lab.DynamoDB.SurveyTest.csproj", "{36FF7374-A6A2-436D-902D-8805336F3A38}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {36FF7374-A6A2-436D-902D-8805336F3A38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {36FF7374-A6A2-436D-902D-8805336F3A38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36FF7374-A6A2-436D-902D-8805336F3A38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {36FF7374-A6A2-436D-902D-8805336F3A38}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/DynamoDB/Lab.DynamoDB/docker-compose.yml b/DynamoDB/Lab.DynamoDB/docker-compose.yml new file mode 100644 index 00000000..f8874b72 --- /dev/null +++ b/DynamoDB/Lab.DynamoDB/docker-compose.yml @@ -0,0 +1,16 @@ +version: "3.8" + +services: + ddb: + image: amazon/dynamodb-local + command: ["-jar", "DynamoDBLocal.jar", "-inMemory", "-sharedDb"] + ports: + - 8000:8000 + ddb-admin: + image: aaronshaf/dynamodb-admin + environment: + - DYNAMO_ENDPOINT=http://ddb:8000 + ports: + - 8005:8001 + depends_on: + - ddb diff --git a/DynamoDB/Lab.DynamoDB/global.json b/DynamoDB/Lab.DynamoDB/global.json new file mode 100644 index 00000000..531745d4 --- /dev/null +++ b/DynamoDB/Lab.DynamoDB/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "3.1.100", + "rollForward": "latestMajor", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/File/Lab.VSF/Lab.FileSystem.TestProject/FileAdapterUnitTests.cs b/File/Lab.VSF/Lab.FileSystem.TestProject/FileAdapterUnitTests.cs new file mode 100644 index 00000000..fdbf7546 --- /dev/null +++ b/File/Lab.VSF/Lab.FileSystem.TestProject/FileAdapterUnitTests.cs @@ -0,0 +1,318 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using Lexical.FileSystem; +using Lexical.FileSystem.Decoration; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.FileSystem.TestProject +{ + [TestClass] + public class FileAdapterUnitTests + { + [TestMethod] + public void FileSystem_DeleteAgo() + { + //arrange + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootFolderPath = Path.GetDirectoryName(executingAssembly.Location); + var targetFolderName = "TestFolder"; + var content = "This is test string"; + + using (var fileSystem = CreateTestFile(rootFolderPath, targetFolderName, content)) + { + var adapter = new FileAdapter(fileSystem); + + //act + adapter.DeleteAgo(targetFolderName, 2); + + //assert + var directoryContent = fileSystem.Browse("targetFolder"); + Assert.AreEqual(true, directoryContent.Any() == false); + + //restore + fileSystem.Delete(targetFolderName, true); + } + } + + [TestMethod] + public void FileSystem_GetContents() + { + //arrange + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootFolderPath = Path.GetDirectoryName(executingAssembly.Location); + + var targetFolderName = "TestFolder"; + var content = "This is test string"; + + using (var fileSystem = CreateTestFile(rootFolderPath, targetFolderName, content)) + { + var adapter = new FileAdapter(fileSystem); + + //act + var actual = adapter.GetContents(targetFolderName); + + //assert + Assert.IsTrue(actual.Count > 0); + + //restore + fileSystem.Delete(targetFolderName, true); + } + } + + [TestMethod] + public void FileSystem_GetFileNames() + { + //arrange + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootFolderPath = Path.GetDirectoryName(executingAssembly.Location); + var targetFolderName = "TestFolder"; + var content = "This is test string"; + + using (var fileSystem = CreateTestFile(rootFolderPath, targetFolderName, content)) + { + var adapter = new FileAdapter(fileSystem); + + //act + var actual = adapter.GetFileNames(targetFolderName); + + //assert + Assert.IsTrue(actual.Count > 0); + + //restore + fileSystem.Delete(targetFolderName, true); + } + } + + [TestMethod] + public void MemoryFileSystem_DeleteAgo() + { + //arrange + var rootFolderPath = "A:\\TestFolder\\Test"; + var content = "This is test string"; + using (var fileSystem = CreateTestMemoryFile(rootFolderPath, content)) + { + var adapter = new FileAdapter(fileSystem); + + //act + adapter.DeleteAgo(rootFolderPath, 2); + + //assert + var directoryContent = fileSystem.Browse(rootFolderPath); + Assert.AreEqual(true, directoryContent.Any() == false); + } + } + + [TestMethod] + public void MemoryFileSystem_GetContents() + { + //arrange + var rootFolderPath = "A:\\TestFolder\\Test"; + var content = "This is test string"; + + using (var fileSystem = CreateTestMemoryFile(rootFolderPath, content)) + { + var adapter = new FileAdapter(fileSystem); + + //act + var actual = adapter.GetContents(rootFolderPath); + + //assert + Assert.IsTrue(actual.Count > 0); + } + } + [TestMethod] + public void MemoryFileSystem_GetFileNames() + { + //arrange + var rootFolderPath = "A:\\TestFolder\\Test"; + var content = "This is test string"; + + using (var fileSystem = CreateTestMemoryFile(rootFolderPath, content)) + { + var adapter = new FileAdapter(fileSystem); + + //act + var actual = adapter.GetFileNames(rootFolderPath); + + //assert + Assert.IsTrue(actual.Count > 0); + } + } + + private static Lexical.FileSystem.FileSystem CreateTestFile(string rootFolder, string subFolder, string content) + { + var fileSystem = new Lexical.FileSystem.FileSystem(rootFolder); + + if (fileSystem.Exists(subFolder) == false) + { + fileSystem.CreateDirectory(subFolder); + } + + for (var i = 0; i < 5; i++) + { + var filePath = Path.Combine(rootFolder, subFolder, $"{i}.txt"); + var contentBytes = Encoding.UTF8.GetBytes($"{i}.{content}"); + fileSystem.CreateFile(filePath, contentBytes); + } + + var now = DateTime.UtcNow.AddDays(-30); + for (var i = 0; i < 5; i++) + { + var filePath = Path.Combine(rootFolder, subFolder, $"{i}.txt"); + File.SetLastWriteTime(filePath, now); + File.SetLastAccessTime(filePath, now); + File.SetCreationTime(filePath, now); + } + + return fileSystem; + } + + private static void CreateTestFile1(string folderPath, string content) + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + + using (var folder = new Lexical.FileSystem.FileSystem(rootPath)) + { + if (folder.Exists(folderPath) == false) + { + folder.CreateDirectory(folderPath); + } + } + + using (var folder = new Lexical.FileSystem.FileSystem($"{rootPath}\\{folderPath}")) + { + for (var i = 0; i < 5; i++) + { + var filePath = $"{i}.txt"; + var contentBytes = Encoding.UTF8.GetBytes($"{i}.{content}"); + + folder.CreateFile(filePath, contentBytes); + } + + folder.PrintTo(Console.Out); + } + } + + private static MemoryFileSystem CreateTestMemoryFile(string folderPath, string content) + { + var fileSystem = new MemoryFileSystem(); + + fileSystem.CreateDirectory(folderPath); + + for (var i = 0; i < 5; i++) + { + var filePath = $"{folderPath}/{i}.txt"; + + // via stream + using (var outputStream = + fileSystem.Open(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + Write(outputStream, $"{i}.{content}"); + } + + // via IFileSystem.Create + // var contentBytes = Encoding.UTF8.GetBytes($"{i}.{content}"); + // fileSystem.CreateFile(filePath, contentBytes); + } + + var type = typeof(FileEntry); + var offset = new DateTimeOffset(DateTime.UtcNow.AddDays(-3)); + foreach (var entry in fileSystem.Browse(folderPath)) + { + var lastAccessPropertyInfo = type.GetProperty("LastAccess"); + var lastModifiedPropertyInfo = type.GetProperty("LastModified"); + lastAccessPropertyInfo.SetValue(entry, offset); + lastModifiedPropertyInfo.SetValue(entry, offset); + } + + return fileSystem; + } + + private static VirtualFileSystem CreateTestVirtualFile(string folderPath, string content) + { + var result = new VirtualFileSystem(); + + result.CreateDirectory(folderPath); + var directory = result.Browse(folderPath); + var folder = directory.FileSystem; + for (var i = 0; i < 5; i++) + { + var filePath = $"{folderPath}/{i}.txt"; + + // var filePath = $"{folderPath}\\{i}.txt"; + + // via stream + using (var outputStream = + folder.Open(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + Write(outputStream, $"{i}.{content}"); + } + + // via IFileSystem.Create + var contentBytes = Encoding.UTF8.GetBytes($"{i}.{content}"); + folder.CreateFile(filePath, contentBytes, new FileProviderSystem.Options()); + } + + var type = typeof(FileEntry); + var offset = new DateTimeOffset(DateTime.UtcNow.AddDays(-3)); + foreach (var entry in result.Browse(folderPath)) + { + var path = entry.Path; + + // var type = entry.GetType(); + + // entry.LastAccess.AddDays(-2); + // entry.LastModified.AddDays(-2); + var lastAccessPropertyInfo = type.GetProperty("LastAccess"); + var lastModifiedPropertyInfo = type.GetProperty("LastModified"); + lastAccessPropertyInfo.SetValue(entry, offset); + lastModifiedPropertyInfo.SetValue(entry, offset); + } + + foreach (var entry in result.Browse(folderPath)) + { + var path = entry.Path; + } + + return result; + } + + private static string Read(Stream stream) + { + var buffer = new byte[1024]; + int length; + var builder = new StringBuilder(); + while ((length = stream.Read(buffer, 0, buffer.Length)) > 0) + { + var content = Encoding.UTF8.GetString(buffer, 0, length); + Console.WriteLine(content); + builder.Append(content); + } + + return builder.ToString(); + } + + private static string Read1(Stream stream) + { + var buffer = new byte[1024]; + var builder = new StringBuilder(); + while (stream.Read(buffer, 0, buffer.Length) > 0) + { + var content = Encoding.UTF8.GetString(buffer, 0, stream.Read(buffer, 0, buffer.Length)); + builder.Append(content); + } + + return buffer.ToString(); + } + + private static void Write(Stream stream, string content) + { + var bytes = Encoding.UTF8.GetBytes(content); + stream.Write(bytes, 0, bytes.Length); + } + } +} \ No newline at end of file diff --git a/File/Lab.VSF/Lab.FileSystem.TestProject/Lab.FileSystem.TestProject.csproj b/File/Lab.VSF/Lab.FileSystem.TestProject/Lab.FileSystem.TestProject.csproj new file mode 100644 index 00000000..6b9f4bef --- /dev/null +++ b/File/Lab.VSF/Lab.FileSystem.TestProject/Lab.FileSystem.TestProject.csproj @@ -0,0 +1,26 @@ + + + + + false + + net5.0 + + + + + + + + + + + + + + + + + + + diff --git a/File/Lab.VSF/Lab.FileSystem.TestProject/SurveyFileSystem.cs b/File/Lab.VSF/Lab.FileSystem.TestProject/SurveyFileSystem.cs new file mode 100644 index 00000000..bffba752 --- /dev/null +++ b/File/Lab.VSF/Lab.FileSystem.TestProject/SurveyFileSystem.cs @@ -0,0 +1,236 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Lexical.FileSystem; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.FileSystem.TestProject +{ + [TestClass] + public class SurveyFileSystem + { + [TestMethod] + public void 列舉根路徑內的所有結構() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var subPath = "TestFolder"; + var subPath1 = $"{subPath}/1"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + Lexical.FileSystem.FileSystem fileSystem = null; + try + { + fileSystem = new Lexical.FileSystem.FileSystem(rootPath); + if (fileSystem.Exists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.Exists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + var file1 = $"{subPath1_1}/1_1.text"; + if (fileSystem.Exists(file1) == false) + { + fileSystem.CreateFile(file1, contentBytes); + } + + var file2 = Path.Combine(rootPath, subPath1_1_1, "1_1_1.txt"); + if (fileSystem.Exists(file2) == false) + { + using var stream = + fileSystem.Open(file2, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + var lines = fileSystem.VisitTree(subPath); + foreach (var line in lines) + { + Console.WriteLine($"{line.Path}"); + } + + fileSystem.PrintTo(Console.Out, subPath); + } + finally + { + if (fileSystem != null) + { + //還原 + fileSystem.Delete(subPath, true); + fileSystem.Dispose(); + } + } + } + + [TestMethod] + public void 列舉根路徑內的子資料夾() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var subPath = "TestFolder"; + var subPath1 = $"{subPath}/1"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + Lexical.FileSystem.FileSystem fileSystem = null; + try + { + fileSystem = new Lexical.FileSystem.FileSystem(rootPath); + if (fileSystem.Exists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.Exists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + var file1 = $"{subPath1_1}/1_1.text"; + if (fileSystem.Exists(file1) == false) + { + fileSystem.CreateFile(file1, contentBytes); + } + + var file2 = Path.Combine(rootPath, subPath1_1_1, "1_1_1.txt"); + if (fileSystem.Exists(file2) == false) + { + using var stream = + fileSystem.Open(file2, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + fileSystem.PrintTo(Console.Out, subPath); + + foreach (var entry in fileSystem.Browse(subPath)) + { + var path = entry.Path; + Console.WriteLine(path); + } + } + finally + { + if (fileSystem != null) + { + //還原 + fileSystem.Delete(subPath, true); + fileSystem.Dispose(); + } + } + } + + [TestMethod] + public void 在資料夾建立檔案() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var subPath = "TestFolder"; + var subPath1 = $"{subPath}/1"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + Lexical.FileSystem.FileSystem fileSystem = null; + try + { + fileSystem = new Lexical.FileSystem.FileSystem(rootPath); + if (fileSystem.Exists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.Exists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + var file1 = $"{subPath1_1}/1_1.text"; + if (fileSystem.Exists(file1) == false) + { + fileSystem.CreateFile(file1, contentBytes); + } + + var file2 = Path.Combine(rootPath, subPath1_1_1, "1_1_1.txt"); + if (fileSystem.Exists(file2) == false) + { + using var stream = + fileSystem.Open(file2, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + fileSystem.PrintTo(Console.Out, subPath); + } + finally + { + if (fileSystem != null) + { + //還原 + fileSystem.Delete(subPath, true); + fileSystem.Dispose(); + } + } + } + + [TestMethod] + public void 建立資料夾() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var subPath = "TestFolder"; + var subPath1 = $"{subPath}/1"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + + Lexical.FileSystem.FileSystem fileSystem = null; + try + { + fileSystem = new Lexical.FileSystem.FileSystem(rootPath); + if (fileSystem.Exists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.Exists(subPath1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1); + } + + if (fileSystem.Exists(subPath1) == false) + { + fileSystem.CreateDirectory(subPath1); + } + + if (fileSystem.Exists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + fileSystem.PrintTo(Console.Out, subPath); + } + finally + { + if (fileSystem != null) + { + //還原 + fileSystem.Delete(subPath, true); + fileSystem.Dispose(); + } + } + } + } +} \ No newline at end of file diff --git a/File/Lab.VSF/Lab.FileSystem.TestProject/SurveyMemoryFileSystem.cs b/File/Lab.VSF/Lab.FileSystem.TestProject/SurveyMemoryFileSystem.cs new file mode 100644 index 00000000..ad32e777 --- /dev/null +++ b/File/Lab.VSF/Lab.FileSystem.TestProject/SurveyMemoryFileSystem.cs @@ -0,0 +1,266 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Lexical.FileSystem; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.FileSystem.TestProject +{ + [TestClass] + public class SurveyMemoryFileSystem + { + [TestMethod] + public void MemoryFileSystem_FolderStruct() + { + var folderPath = "A:\\TestFolder\\Test"; + var content = "This is test string"; + + using var fileSystem = CreateTestMemoryFile(folderPath, content); + fileSystem.PrintTo(Console.Out); + var adapter = new FileAdapter(fileSystem); + var actual = adapter.GetFileNames(folderPath); + Assert.IsTrue(actual.Count > 0); + } + + [TestMethod] + [Ignore] + public void VirtualFileSystem_ModifyFileDate() + { + IFileSystem fileSystem = new VirtualFileSystem() + .Mount("tmp/", Lexical.FileSystem.FileSystem.Temp) + .Mount("ram/", MemoryFileSystem.Instance); + + var directoryContent = fileSystem.Browse("tmp/"); + var type = typeof(FileEntry); + var offset = new DateTimeOffset(DateTime.UtcNow.AddDays(-3)); + + foreach (var entry in fileSystem.Browse("tmp/")) + { + var fileName = entry.Name; + var filePath = entry.Path; + var lastAccessPropertyInfo = type.GetProperty("LastAccess"); + var lastModifiedPropertyInfo = type.GetProperty("LastModified"); + lastAccessPropertyInfo.SetValue(entry, offset); + lastModifiedPropertyInfo.SetValue(entry, offset); + } + } + + [TestMethod] + public void 列舉根路徑底下所有結構() + { + using var fileSystem = new MemoryFileSystem(); + Console.WriteLine("建立資料夾"); + fileSystem.CreateDirectory("dir1/dir2/dir3/"); + fileSystem.PrintTo(Console.Out); + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes($"{content}"); + + Console.WriteLine("dir1 底下建立檔案"); + using (var outputStream = + fileSystem.Open("dir1/1.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + var bytes = Encoding.UTF8.GetBytes(content); + outputStream.Write(bytes, 0, bytes.Length); + } + + Console.WriteLine("dir2 底下建立檔案"); + fileSystem.CreateFile("dir1/dir2/2.txt", contentBytes); + var tree = fileSystem.VisitTree(); + + foreach (var line in tree) + { + Console.WriteLine($"name:{line.Name},path:{line.Path}"); + } + } + + [TestMethod] + public void 列舉根路徑內的子資料夾() + { + using var fileSystem = new MemoryFileSystem(); + Console.WriteLine("建立資料夾"); + fileSystem.CreateDirectory("dir1/dir2/dir3/"); + fileSystem.PrintTo(Console.Out); + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes($"{content}"); + + Console.WriteLine("dir1 底下建立檔案"); + using (var outputStream = + fileSystem.Open("dir1/1.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + var bytes = Encoding.UTF8.GetBytes(content); + outputStream.Write(bytes, 0, bytes.Length); + } + + Console.WriteLine("dir2 底下建立檔案"); + fileSystem.CreateFile("dir1/dir2/2.txt", contentBytes); + + foreach (var entry in fileSystem.Browse("")) + { + var path = entry.Path; + Console.WriteLine(path); + } + } + + [TestMethod] + public void 在資料夾內建立檔案() + { + using var fileSystem = new MemoryFileSystem(); + Console.WriteLine("建立資料夾"); + fileSystem.CreateDirectory("dir1/dir2/dir3/"); + fileSystem.PrintTo(Console.Out); + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes($"{content}"); + + Console.WriteLine("dir1 底下建立檔案"); + using (var outputStream = + fileSystem.Open("dir1/1.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + var bytes = Encoding.UTF8.GetBytes(content); + outputStream.Write(bytes, 0, bytes.Length); + } + + Console.WriteLine("dir2 底下建立檔案"); + fileSystem.CreateFile("dir1/dir2/2.txt", contentBytes); + + fileSystem.PrintTo(Console.Out); + } + + [TestMethod] + public void 刪除資料夾() + { + using var fileSystem = new MemoryFileSystem(); + Console.WriteLine("建立資料夾"); + fileSystem.CreateDirectory("dir1/dir2/dir3/"); + fileSystem.PrintTo(Console.Out); + + Console.WriteLine("刪除 dir2 資料夾"); + fileSystem.Delete("dir1/dir2/", true); + fileSystem.PrintTo(Console.Out); + } + + [TestMethod] + public void 建立資料夾() + { + using var fileSystem = new MemoryFileSystem(); + Console.WriteLine("建立資料夾"); + fileSystem.CreateDirectory("dir1/dir2/dir3/"); + fileSystem.PrintTo(Console.Out); + } + + [TestMethod] + public void 修改真實檔案日期() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootFolderPath = Path.GetDirectoryName(executingAssembly.Location); + var subFolder = "TestFolder"; + var content = "This is test string"; + + if (Directory.Exists(subFolder) == false) + { + Directory.CreateDirectory(subFolder); + } + + for (var i = 0; i < 5; i++) + { + var filePath = Path.Combine(rootFolderPath, subFolder, $"{i}.txt"); + + var contentBytes = Encoding.UTF8.GetBytes($"{i}.{content}"); + File.WriteAllBytes(filePath, contentBytes); + } + + //修改日期 + for (var i = 0; i < 5; i++) + { + var filePath = Path.Combine(rootFolderPath, subFolder, $"{i}.txt"); + File.SetCreationTime(filePath, new DateTime(2021, 1, 1)); + File.SetLastWriteTime(filePath, new DateTime(2021, 1, 1)); + File.SetLastAccessTime(filePath, new DateTime(2021, 1, 1)); + } + + //刪除檔案 + using var fileSystem = new Lexical.FileSystem.FileSystem(rootFolderPath); + fileSystem.Delete(Path.Combine(subFolder), true); + } + + [TestMethod] + public void 修改檔案日期() + { + using var fileSystem = new MemoryFileSystem(); + Console.WriteLine("建立資料夾"); + fileSystem.CreateDirectory("dir1/dir2/dir3/"); + fileSystem.PrintTo(Console.Out); + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes($"{content}"); + + Console.WriteLine("dir2 底下建立檔案"); + fileSystem.CreateFile("dir1/dir2/2.txt", contentBytes); + + var entry = fileSystem.GetEntry("dir1/dir2/2.txt"); + Console.WriteLine("檔案修改前的日期"); + Console.WriteLine($"LastAccess:{entry.LastAccess}"); + Console.WriteLine($"LastModified:{entry.LastModified}"); + + var type = entry.GetType(); + var now = new DateTimeOffset(DateTime.UtcNow.AddDays(-30)); + var lastAccessPropertyInfo = type.GetProperty("LastAccess"); + var lastModifiedPropertyInfo = type.GetProperty("LastModified"); + lastAccessPropertyInfo.SetValue(entry, now); + lastModifiedPropertyInfo.SetValue(entry, now); + + Console.WriteLine("檔案修改後的日期"); + Console.WriteLine($"LastAccess:{entry.LastAccess}"); + Console.WriteLine($"LastModified:{entry.LastModified}"); + } + + private static MemoryFileSystem CreateTestMemoryFile(string folderPath, string content) + { + var memoryFileSystem = new MemoryFileSystem(); + + memoryFileSystem.CreateDirectory(folderPath); + var directory = memoryFileSystem.Browse(folderPath); + var folder = directory.FileSystem; + + for (var i = 0; i < 5; i++) + { + var filePath = $"{folderPath}/{i}.txt"; + + // var filePath = $"{folderPath}\\{i}.txt"; + + // via stream + using (var outputStream = + folder.Open(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + Write(outputStream, $"{i}.{content}"); + } + + // via IFileSystem.Create + var contentBytes = Encoding.UTF8.GetBytes($"{i}.{content}"); + folder.CreateFile(filePath, contentBytes); + } + + var type = typeof(FileEntry); + var offset = new DateTimeOffset(DateTime.UtcNow.AddDays(-3)); + foreach (var entry in memoryFileSystem.Browse(folderPath)) + { + var lastAccessPropertyInfo = type.GetProperty("LastAccess"); + var lastModifiedPropertyInfo = type.GetProperty("LastModified"); + lastAccessPropertyInfo.SetValue(entry, offset); + lastModifiedPropertyInfo.SetValue(entry, offset); + } + + foreach (var entry in memoryFileSystem.Browse(folderPath)) + { + var path = entry.Path; + } + + return memoryFileSystem; + } + + private static void Write(Stream stream, string content) + { + var bytes = Encoding.UTF8.GetBytes(content); + stream.Write(bytes, 0, bytes.Length); + } + } +} \ No newline at end of file diff --git a/File/Lab.VSF/Lab.FileSystem.TestProject/SurveyVirtualFileSystem.cs b/File/Lab.VSF/Lab.FileSystem.TestProject/SurveyVirtualFileSystem.cs new file mode 100644 index 00000000..7e8cd941 --- /dev/null +++ b/File/Lab.VSF/Lab.FileSystem.TestProject/SurveyVirtualFileSystem.cs @@ -0,0 +1,119 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Lexical.FileSystem; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.FileSystem.TestProject +{ + [TestClass] + public class SurveyVirtualFileSystem + { + [TestMethod] + public void Mount() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var subPath = "TestFolder"; + + using var fileSystem = CreateFolder(rootPath, subPath); + using var virtualFileSystem = new VirtualFileSystem(); + using var memoryFileSystem = new MemoryFileSystem(); + + Console.WriteLine("掛載到虛擬結構..."); + var appDir = rootPath.Replace('\\', '/'); + + // virtualFileSystem.Mount("", new Lexical.FileSystem.FileSystem(appDir), Option.SubPath(appDir)); + virtualFileSystem.Mount("", Lexical.FileSystem.FileSystem.OS, Option.SubPath(appDir)); + + //操作會對應到真實檔案 + virtualFileSystem.CreateDirectory($"/{subPath}/AAA"); + Console.WriteLine("virtualFileSystem"); + virtualFileSystem.PrintTo(Console.Out); + } + + [TestMethod] + public void 映射資料結構() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var subPath = "TestFolder"; + + var fileSystem = CreateFolder(rootPath, subPath); + var virtualFileSystem = new VirtualFileSystem(); + var memoryFileSystem = new MemoryFileSystem(); + + var appDir = rootPath.Replace('\\', '/'); + virtualFileSystem.Mount("", Lexical.FileSystem.FileSystem.OS, Option.SubPath(appDir)); + virtualFileSystem.CopyTree($"/{subPath}/", memoryFileSystem, ""); + memoryFileSystem.CreateDirectory("AAA"); + Console.WriteLine("memoryFileSystem"); + memoryFileSystem.PrintTo(Console.Out); + } + + [TestMethod] + public void 映射資料結構2() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var subPath = "TestFolder"; + + using var fileSystem = CreateFolder(rootPath, subPath); + using var memoryFileSystem = new MemoryFileSystem(); + + foreach (var line in fileSystem.VisitTree(subPath)) + { + if (line.Entry.IsDirectory()) + { + memoryFileSystem.CreateDirectory(line.Path); + } + + if (line.Entry.IsFile()) + { + fileSystem.CopyFile(line.Path, memoryFileSystem, line.Path); + } + } + + memoryFileSystem.CreateDirectory("AAA"); + memoryFileSystem.PrintTo(Console.Out); + } + + private static Lexical.FileSystem.FileSystem CreateFolder(string rootPath, string subPath) + { + var subPath1 = $"{subPath}/1"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + var fileSystem = new Lexical.FileSystem.FileSystem(rootPath); + if (fileSystem.Exists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.Exists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + var file1 = $"{subPath1_1}/1_1.text"; + if (fileSystem.Exists(file1) == false) + { + fileSystem.CreateFile(file1, contentBytes); + } + + var file2 = Path.Combine(rootPath, subPath1_1_1, "1_1_1.txt"); + if (fileSystem.Exists(file2) == false) + { + using var stream = + fileSystem.Open(file2, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + return fileSystem; + } + } +} \ No newline at end of file diff --git a/File/Lab.VSF/Lab.FileSystem/FileAdapter.cs b/File/Lab.VSF/Lab.FileSystem/FileAdapter.cs new file mode 100644 index 00000000..8b8013f4 --- /dev/null +++ b/File/Lab.VSF/Lab.FileSystem/FileAdapter.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Lexical.FileSystem; + +namespace Lab.FileSystem +{ + public class FileAdapter : IFileAdapter + { + internal DateTime Now + { + get + { + if (this._now.HasValue == false) + { + return DateTime.UtcNow; + } + + return this._now.Value; + } + set => this._now = value; + } + + private readonly IFileSystem _fileSystem; + private DateTime? _now; + + public FileAdapter(IFileSystem fileSystem) + { + this._fileSystem = fileSystem; + } + + public Dictionary GetContents(string folderPath) + { + var fileSystem = this._fileSystem; + + var results = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + if (fileSystem.Exists(folderPath) == false) + { + return results; + } + + foreach (var entry in fileSystem.Browse(folderPath)) + { + var path = entry.Path; + + using (var inputStream = entry.FileSystem.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var content = Read(inputStream); + + results.Add(path, content); + } + } + + return results; + } + + public ICollection GetFileNames(string folderPath) + { + var fileSystem = this._fileSystem; + + var results = new List(); + + // if (fileSystem.Browse(folderPath).Exists ==false) + // { + // return results; + // } + if (fileSystem.Exists(folderPath) == false) + { + return results; + } + + foreach (var entry in fileSystem.Browse(folderPath)) + { + results.Add(entry.Path); + } + + return results; + } + + public void DeleteAgo(string folderName, int day) + { + var fileSystem = this._fileSystem; + var now = this.Now; + + if (fileSystem.Exists(folderName) == false) + { + return; + } + + foreach (var entry in fileSystem.Browse(folderName)) + { + var diff = now - entry.LastModified.Date; + if (diff.Days > day) + { + fileSystem.Delete(entry.Path); + Console.WriteLine($"Delete:{entry.Path}"); + } + } + } + + private static string Read(Stream stream) + { + var buffer = new byte[1024]; + int length; + var builder = new StringBuilder(); + while ((length = stream.Read(buffer, 0, buffer.Length)) > 0) + { + var content = Encoding.UTF8.GetString(buffer, 0, length); + Console.WriteLine(content); + builder.Append(content); + } + + return builder.ToString(); + } + } +} \ No newline at end of file diff --git a/File/Lab.VSF/Lab.FileSystem/IFileAdapter.cs b/File/Lab.VSF/Lab.FileSystem/IFileAdapter.cs new file mode 100644 index 00000000..cb0c52ce --- /dev/null +++ b/File/Lab.VSF/Lab.FileSystem/IFileAdapter.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Lab.FileSystem +{ + public interface IFileAdapter + { + void DeleteAgo(string folderName, int day); + + Dictionary GetContents(string folderName); + + ICollection GetFileNames(string folderName); + } +} \ No newline at end of file diff --git a/File/Lab.VSF/Lab.FileSystem/Lab.FileSystem.csproj b/File/Lab.VSF/Lab.FileSystem/Lab.FileSystem.csproj new file mode 100644 index 00000000..dee13845 --- /dev/null +++ b/File/Lab.VSF/Lab.FileSystem/Lab.FileSystem.csproj @@ -0,0 +1,15 @@ + + + + net5.0 + + + + + + + + <_Parameter1>Lab.FileSystem.TestProject + + + diff --git a/File/Lab.VSF/Lab.ServiceStack.FVS.TestProject/Lab.ServiceStack.FVS.TestProject.csproj b/File/Lab.VSF/Lab.ServiceStack.FVS.TestProject/Lab.ServiceStack.FVS.TestProject.csproj new file mode 100644 index 00000000..a141d619 --- /dev/null +++ b/File/Lab.VSF/Lab.ServiceStack.FVS.TestProject/Lab.ServiceStack.FVS.TestProject.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + + false + + + + + + + + + + + + diff --git a/File/Lab.VSF/Lab.ServiceStack.FVS.TestProject/SurveyFileSystemVirtualFilesTests.cs b/File/Lab.VSF/Lab.ServiceStack.FVS.TestProject/SurveyFileSystemVirtualFilesTests.cs new file mode 100644 index 00000000..cb662b1c --- /dev/null +++ b/File/Lab.VSF/Lab.ServiceStack.FVS.TestProject/SurveyFileSystemVirtualFilesTests.cs @@ -0,0 +1,51 @@ +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using ServiceStack.IO; + +namespace Lab.ServiceStack.FVS.TestProject +{ + [TestClass] + public class SurveyFileSystemVirtualFilesTests + { + [TestMethod] + public void 新增資料夾() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var subPath = "TestFolder"; + var subPath1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + + var virtualFiles = new FileSystemVirtualFiles(rootPath); + if (virtualFiles.DirectoryExists(subPath1) == false) + { + virtualFiles.EnsureDirectory(subPath1); + } + + if (virtualFiles.DirectoryExists(subPath2) == false) + { + virtualFiles.EnsureDirectory(subPath2); + } + + virtualFiles.DeleteFolder(subPath); + } + + [TestMethod] + public void 新增檔案() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var subPath = "TestFolder"; + var content = "This is test string"; + + var virtualFiles = new FileSystemVirtualFiles(rootPath); + if (virtualFiles.DirectoryExists(subPath) == false) + { + virtualFiles.EnsureDirectory(subPath); + } + + virtualFiles.AppendFile($"{subPath}/1.txt", content); + } + } +} \ No newline at end of file diff --git a/File/Lab.VSF/Lab.ServiceStack.FVS.TestProject/SurveyMemoryVirtualFilesTests.cs b/File/Lab.VSF/Lab.ServiceStack.FVS.TestProject/SurveyMemoryVirtualFilesTests.cs new file mode 100644 index 00000000..8c2f4691 --- /dev/null +++ b/File/Lab.VSF/Lab.ServiceStack.FVS.TestProject/SurveyMemoryVirtualFilesTests.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using ServiceStack.IO; + +namespace Lab.ServiceStack.FVS.TestProject +{ + [TestClass] + public class SurveyMemoryVirtualFilesTests + { + [TestMethod] + public void 新增資料夾() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var content = "This is test string"; + var subPath = "TestFolder"; + var subPath1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var fileSystem = new FileSystemVirtualFiles(rootPath); + fileSystem.EnsureDirectory(subPath1); + fileSystem.EnsureDirectory(subPath2); + fileSystem.AppendFile($"{subPath}/1.txt", content); + var memoryFileSystem = new MemoryVirtualFiles(); + + // var memoryFileSystem1 = fileSystem.GetMemoryVirtualFiles(); + + // var nonDefaultValues = fileSystem.PopulateWithNonDefaultValues(memoryFileSystem); + // var memoryFileSystem2 = memoryFileSystem.PopulateWith(fileSystem); + + var subFolder = new InMemoryVirtualDirectory(memoryFileSystem, subPath); + var subFile = new InMemoryVirtualFile(memoryFileSystem, subFolder); + memoryFileSystem.AddFile(subFile); + + //無法單獨加入資料夾 + var subFolder1 = new InMemoryVirtualDirectory(memoryFileSystem, "1", subFolder); + + var subFolder2 = new InMemoryVirtualDirectory(memoryFileSystem, "1_1", subFolder1); + subFolder2.AddFile("2.txt", content); + + var directories = memoryFileSystem.RootDirectory.Directories; + var files = memoryFileSystem.Files; + Console.WriteLine(); + + // + // // memorySystem.AddFile(new InMemoryVirtualFile(fileSystem, directory)); + // + // // memorySystem.AppendFile($"{subPath1}/1.txt",content); + // var files = memoryFileSystem.GetAllFiles(); + } + } +} \ No newline at end of file diff --git a/File/Lab.VSF/Lab.VSF.sln b/File/Lab.VSF/Lab.VSF.sln new file mode 100644 index 00000000..fbbb3014 --- /dev/null +++ b/File/Lab.VSF/Lab.VSF.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.FileSystem", "Lab.FileSystem\Lab.FileSystem.csproj", "{B0D5EEBE-E01A-4A12-96AD-149BE28573BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.FileSystem.TestProject", "Lab.FileSystem.TestProject\Lab.FileSystem.TestProject.csproj", "{66B51546-67B1-4000-A643-5E8C41CE1431}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ServiceStack.FVS.TestProject", "Lab.ServiceStack.FVS.TestProject\Lab.ServiceStack.FVS.TestProject.csproj", "{338CAECF-3379-4EB8-B824-FE0E20B6F0FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ZIO.TestProject", "Lab.ZIO.TestProject\Lab.ZIO.TestProject.csproj", "{B0A52BD4-1710-47E8-86F7-9B4A2B460248}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B0D5EEBE-E01A-4A12-96AD-149BE28573BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0D5EEBE-E01A-4A12-96AD-149BE28573BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0D5EEBE-E01A-4A12-96AD-149BE28573BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0D5EEBE-E01A-4A12-96AD-149BE28573BA}.Release|Any CPU.Build.0 = Release|Any CPU + {66B51546-67B1-4000-A643-5E8C41CE1431}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66B51546-67B1-4000-A643-5E8C41CE1431}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66B51546-67B1-4000-A643-5E8C41CE1431}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66B51546-67B1-4000-A643-5E8C41CE1431}.Release|Any CPU.Build.0 = Release|Any CPU + {338CAECF-3379-4EB8-B824-FE0E20B6F0FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {338CAECF-3379-4EB8-B824-FE0E20B6F0FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {338CAECF-3379-4EB8-B824-FE0E20B6F0FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {338CAECF-3379-4EB8-B824-FE0E20B6F0FD}.Release|Any CPU.Build.0 = Release|Any CPU + {B0A52BD4-1710-47E8-86F7-9B4A2B460248}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0A52BD4-1710-47E8-86F7-9B4A2B460248}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0A52BD4-1710-47E8-86F7-9B4A2B460248}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0A52BD4-1710-47E8-86F7-9B4A2B460248}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/File/Lab.VSF/Lab.ZIO.TestProject/Lab.ZIO.TestProject.csproj b/File/Lab.VSF/Lab.ZIO.TestProject/Lab.ZIO.TestProject.csproj new file mode 100644 index 00000000..88f37caf --- /dev/null +++ b/File/Lab.VSF/Lab.ZIO.TestProject/Lab.ZIO.TestProject.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + + false + + + + + + + + + + + diff --git a/File/Lab.VSF/Lab.ZIO.TestProject/SurveyMemoryFileSystemTests.cs b/File/Lab.VSF/Lab.ZIO.TestProject/SurveyMemoryFileSystemTests.cs new file mode 100644 index 00000000..8652c274 --- /dev/null +++ b/File/Lab.VSF/Lab.ZIO.TestProject/SurveyMemoryFileSystemTests.cs @@ -0,0 +1,255 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Zio; +using Zio.FileSystems; + +namespace Lab.ZIO.TestProject +{ + [TestClass] + public class SurveyMemoryFileSystemTests + { + [TestMethod] + public void UPathCombine() + { + var rootPath = "/mnt/c/Temp/Test"; + var uPath1 = UPath.Combine(rootPath, "../1"); + var uPath2 = UPath.Combine(rootPath, "./2/"); + var uPath3 = UPath.Combine(rootPath, ".."); + var uPath4 = UPath.Combine(rootPath, @"..\..\3\"); + var uPath5 = (UPath) "/this/is/a/path/to/a/directory"; + var uPath6 = (UPath) @"/this\is/wow/../an/absolute/./pat/h/"; + + Console.WriteLine(uPath1); + Console.WriteLine(uPath2); + Console.WriteLine(uPath3); + Console.WriteLine(uPath4); + Console.WriteLine(uPath5); + Console.WriteLine(uPath6); + } + [TestMethod] + public void UPathTo() + { + var rootPath = "/mnt/c/Temp/Test"; + var path1 = (UPath) "/this/is/a/path/to/a/directory"; + var path2 = (UPath) @"/this\is/wow/../an/absolute/./pat/h/"; + var path3 = (UPath) @"this\is/wow/../an/absolute/./pat/h/"; + + Console.WriteLine(path1); + Console.WriteLine(path2); + Console.WriteLine(path3); + } + + [TestMethod] + public void PathCombine() + { + var rootPath = @"E:\src\sample.dotblog\File"; + var path1 = Path.Combine(rootPath, "../1"); + var path2 = Path.Combine(rootPath, "./2/"); + var path3 = Path.Combine(rootPath, ".."); + var path4 = Path.Combine(rootPath, @"..\..\3\"); + + Console.WriteLine(new DirectoryInfo(path1).FullName); + Console.WriteLine(new DirectoryInfo(path2).FullName); + Console.WriteLine(new DirectoryInfo(path3).FullName); + Console.WriteLine(new DirectoryInfo(path4).FullName); + } + + [TestMethod] + public void 列舉根路徑內的子資料夾() + { + var rootUPath = CreateRootPath(); + using var fileSystem = new MemoryFileSystem(); + var subName = "../../path"; + + var subPath = $"{rootUPath}/{subName}"; + var subPath1 = $"{subPath}/1"; + var subFile1 = $"{subPath}/1/1.txt"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subFile1_1 = $"{subPath}/1/1_1/1_1.txt"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + + var uPath = UPath.Combine(rootUPath, ".."); + + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + if (fileSystem.DirectoryExists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.DirectoryExists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + if (fileSystem.FileExists(subFile1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + if (fileSystem.FileExists(subFile1_1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1_1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + var directoryEntries = fileSystem.EnumerateDirectoryEntries(subPath); + foreach (var entry in directoryEntries) + { + Console.WriteLine(entry.Path); + } + + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath2)); + } + + [TestMethod] + public void 在資料夾建立檔案() + { + var rootUPath = CreateRootPath(); + using var fileSystem = new MemoryFileSystem(); + + var subName = "TestFolder"; + var subPath = $"{rootUPath}/{subName}"; + var subPath1 = $"{subPath}/1"; + var subFile1 = $"{subPath}/1/1.txt"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subFile1_1 = $"{subPath}/1/1_1/1_1.txt"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + if (fileSystem.DirectoryExists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.DirectoryExists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + if (fileSystem.FileExists(subFile1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + if (fileSystem.FileExists(subFile1_1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1_1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath2)); + } + + [TestMethod] + public void 建立資料夾() + { + var rootUPath = CreateRootPath(); + using var fileSystem = new MemoryFileSystem(); + + var subName = "TestFolder"; + var subPath = $"{rootUPath}/{subName}"; + var subPath1 = $"{subPath}/1"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + + if (fileSystem.DirectoryExists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.DirectoryExists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath2)); + } + + [TestMethod] + public void 修改檔案日期() + { + var rootUPath = CreateRootPath(); + using var fileSystem = new MemoryFileSystem(); + + var subName = "TestFolder"; + + var subPath = $"{rootUPath}/{subName}"; + var subPath1 = $"{subPath}/1"; + var subFile1 = $"{subPath}/1/1.txt"; + var subFile2 = $"{subPath}/1/2.txt"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subFile1_1 = $"{subPath}/1/1_1/1_1.txt"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + if (fileSystem.DirectoryExists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.DirectoryExists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + if (fileSystem.FileExists(subFile1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + if (fileSystem.FileExists(subFile1_1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1_1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + var fileEntry = fileSystem.GetFileEntry(subFile1); + fileEntry.CreationTime = new DateTime(1900, 1, 1); + fileEntry.LastWriteTime = new DateTime(1900, 1, 2); + fileEntry.LastAccessTime = new DateTime(1900, 1, 3); + + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath2)); + + fileSystem.DeleteDirectory(subPath, true); + } + + private static UPath CreateRootPath() + { + using var fileSystem = new PhysicalFileSystem(); + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + return fileSystem.ConvertPathFromInternal(rootPath); + } + } +} \ No newline at end of file diff --git a/File/Lab.VSF/Lab.ZIO.TestProject/SurveyPhysicalFileSystemTests.cs b/File/Lab.VSF/Lab.ZIO.TestProject/SurveyPhysicalFileSystemTests.cs new file mode 100644 index 00000000..9a6dd7be --- /dev/null +++ b/File/Lab.VSF/Lab.ZIO.TestProject/SurveyPhysicalFileSystemTests.cs @@ -0,0 +1,319 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Zio; +using Zio.FileSystems; + +namespace Lab.ZIO.TestProject +{ + [TestClass] + public class SurveyPhysicalFileSystemTests + { + [TestMethod] + public void aaa() + { + using var fileSystem = new PhysicalFileSystem(); + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var rootUPath = fileSystem.ConvertPathFromInternal(rootPath); + var rootUPath1 = fileSystem.ConvertPathToInternal(rootUPath); + var subName = "TestFolder"; + + var subPath = $"{rootUPath}/{subName}"; + var subPath1 = $"{subPath}/1"; + var subFile1 = $"{subPath}/1/1.txt"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subFile1_1 = $"{subPath}/1/1_1/1_1.txt"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + if (fileSystem.DirectoryExists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.DirectoryExists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + if (fileSystem.FileExists(subFile1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + if (fileSystem.FileExists(subFile1_1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1_1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + var directoryEntries = fileSystem.EnumerateDirectoryEntries(subPath); + foreach (var entry in directoryEntries) + { + Console.WriteLine(entry.Path); + } + + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath2)); + + fileSystem.DeleteDirectory(subPath, true); + } + + [TestMethod] + public void 列舉根路徑內的子資料夾() + { + using var fileSystem = new PhysicalFileSystem(); + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var rootUPath = fileSystem.ConvertPathFromInternal(rootPath); + var rootUPath1 = fileSystem.ConvertPathToInternal(rootUPath); + var subName = "TestFolder"; + + var subPath = $"{rootUPath}/{subName}"; + var subPath1 = $"{subPath}/1"; + var subFile1 = $"{subPath}/1/1.txt"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subFile1_1 = $"{subPath}/1/1_1/1_1.txt"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + if (fileSystem.DirectoryExists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.DirectoryExists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + if (fileSystem.FileExists(subFile1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + if (fileSystem.FileExists(subFile1_1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1_1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + var directoryEntries = fileSystem.EnumerateDirectoryEntries(subPath); + foreach (var entry in directoryEntries) + { + Console.WriteLine(entry.Path); + } + + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath2)); + + fileSystem.DeleteDirectory(subPath, true); + } + + [TestMethod] + public void 在資料夾建立檔案() + { + using var fileSystem = new PhysicalFileSystem(); + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var rootUPath = fileSystem.ConvertPathFromInternal(rootPath); + + var subName = "TestFolder"; + var subPath = $"{rootUPath}/{subName}"; + var subPath1 = $"{subPath}/1"; + var subFile1 = $"{subPath}/1/1.txt"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subFile1_1 = $"{subPath}/1/1_1/1_1.txt"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + if (fileSystem.DirectoryExists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.DirectoryExists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + if (fileSystem.FileExists(subFile1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + if (fileSystem.FileExists(subFile1_1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1_1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath2)); + + fileSystem.DeleteDirectory(subPath, true); + } + + [TestMethod] + public void 建立資料夾() + { + using var fileSystem = new PhysicalFileSystem(); + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var rootUPath = fileSystem.ConvertPathFromInternal(rootPath); + + var subName = "TestFolder"; + var subPath = $"{rootUPath}/{subName}"; + var subPath1 = $"{subPath}/1"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + + if (fileSystem.DirectoryExists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.DirectoryExists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath2)); + fileSystem.DeleteDirectory(subPath, true); + } + + [TestMethod] + public void 修改資料夾時間() + { + using var fileSystem = new PhysicalFileSystem(); + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var rootUPath = fileSystem.ConvertPathFromInternal(rootPath); + + var subName = "TestFolder"; + var subPath = $"{rootUPath}/{subName}"; + var subPath1 = $"{subPath}/1"; + var subFile1 = $"{subPath}/1/1.txt"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subFile1_1 = $"{subPath}/1/1_1/1_1.txt"; + var subPath1_1_1 = (UPath) $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + if (fileSystem.DirectoryExists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + var directoryEntry = fileSystem.GetDirectoryEntry(subPath1_1_1); + directoryEntry.CreationTime = new DateTime(2000, 1, 1); + if (fileSystem.DirectoryExists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + if (fileSystem.FileExists(subFile1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + if (fileSystem.FileExists(subFile1_1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1_1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath2)); + + fileSystem.DeleteDirectory(subPath, true); + } + + [TestMethod] + public void 修改檔案日期() + { + using var fileSystem = new PhysicalFileSystem(); + var executingAssembly = Assembly.GetExecutingAssembly(); + var rootPath = Path.GetDirectoryName(executingAssembly.Location); + var rootUPath = fileSystem.ConvertPathFromInternal(rootPath); + var subName = "TestFolder"; + + var subPath = $"{rootUPath}/{subName}"; + var subPath1 = $"{subPath}/1"; + var subFile1 = $"{subPath}/1/1.txt"; + var subFile2 = $"{subPath}/1/2.txt"; + var subPath1_1 = $"{subPath}/1/1_1"; + var subFile1_1 = $"{subPath}/1/1_1/1_1.txt"; + var subPath1_1_1 = $"{subPath}/1/1_1/1_1_1"; + var subPath2 = $"{subPath}/2"; + var content = "This is test string"; + var contentBytes = Encoding.UTF8.GetBytes(content); + if (fileSystem.DirectoryExists(subPath1_1_1) == false) + { + fileSystem.CreateDirectory(subPath1_1_1); + } + + if (fileSystem.DirectoryExists(subPath2) == false) + { + fileSystem.CreateDirectory(subPath2); + } + + if (fileSystem.FileExists(subFile1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + if (fileSystem.FileExists(subFile1_1) == false) + { + using var stream = + fileSystem.OpenFile(subFile1_1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + stream.Write(contentBytes, 0, contentBytes.Length); + } + + var fileEntry = fileSystem.GetFileEntry(subFile1); + fileEntry.CreationTime = new DateTime(1900, 1, 1); + fileEntry.LastWriteTime = new DateTime(1900, 1, 2); + fileEntry.LastAccessTime = new DateTime(1900, 1, 3); + + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath1_1_1)); + Assert.AreEqual(true, fileSystem.DirectoryExists(subPath2)); + + fileSystem.DeleteDirectory(subPath, true); + } + } +} \ No newline at end of file diff --git a/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/GracefulShutdownService.cs b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/GracefulShutdownService.cs new file mode 100644 index 00000000..0b5a46eb --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/GracefulShutdownService.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Hosting; + +namespace Lab.GracefulShutdown.Net6; + +internal class GracefulShutdownService : IHostedService +{ + private readonly IHostApplicationLifetime _appLifetime; + private Task _backgroundTask; + private bool _stop; + + public GracefulShutdownService(IHostApplicationLifetime appLifetime) + { + this._appLifetime = appLifetime; + } + + public Task StartAsync(CancellationToken cancel) + { + Console.WriteLine($"{DateTime.Now} 服務啟動中..."); + + this._backgroundTask = Task.Run(async () => { await this.ExecuteAsync(cancel); }, cancel); + + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancel) + { + Console.WriteLine($"{DateTime.Now} 服務停止中..."); + + this._stop = true; + await this._backgroundTask; + + Console.WriteLine($"{DateTime.Now} 服務已停止"); + } + + private async Task ExecuteAsync(CancellationToken cancel) + { + Console.WriteLine($"{DateTime.Now} 服務已啟動!"); + + while (!this._stop) + { + Console.WriteLine($"{DateTime.Now} 服務運行中..."); + await Task.Delay(TimeSpan.FromSeconds(1), cancel); + } + + Console.WriteLine($"{DateTime.Now} 服務已完美的停止(Graceful Shutdown)"); + } +} \ No newline at end of file diff --git a/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/GracefulShutdownService_Fail.cs b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/GracefulShutdownService_Fail.cs new file mode 100644 index 00000000..38004cce --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/GracefulShutdownService_Fail.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Hosting; + +namespace Lab.GracefulShutdown.Net6; + +internal class GracefulShutdownService_Fail : IHostedService +{ + private readonly IHostApplicationLifetime _appLifetime; + private bool _stop; + + public GracefulShutdownService_Fail(IHostApplicationLifetime appLifetime) + { + this._appLifetime = appLifetime; + } + + public async Task StartAsync(CancellationToken cancel) + { + Console.WriteLine($"{DateTime.Now} 服務啟動中..."); + await this.ExecuteAsync(cancel); + } + + public Task StopAsync(CancellationToken cancel) + { + this._stop = true; + Console.WriteLine("服務關閉"); + return Task.CompletedTask; + } + + private async Task ExecuteAsync(CancellationToken cancel) + { + Console.WriteLine($"{DateTime.Now} 服務已啟動!"); + + while (!this._stop) + { + Console.WriteLine($"{DateTime.Now} 服務運行中..."); + await Task.Delay(TimeSpan.FromSeconds(1), cancel); + } + + Console.WriteLine($"{DateTime.Now} 服務已完美的停止(Graceful Shutdown)"); + } +} \ No newline at end of file diff --git a/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Lab.GracefulShutdown.Net6.csproj b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Lab.GracefulShutdown.Net6.csproj new file mode 100644 index 00000000..1a94648c --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Lab.GracefulShutdown.Net6.csproj @@ -0,0 +1,13 @@ + + + + Exe + net6.0 + enable + enable + + + + + + diff --git a/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Program.cs b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Program.cs new file mode 100644 index 00000000..bec06acf --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Program.cs @@ -0,0 +1,53 @@ +using Lab.GracefulShutdown.Net6; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Runtime.Loader; + +var tcs = new TaskCompletionSource(); +var sigintReceived = false; + +Console.WriteLine("等待以下訊號 SIGINT/SIGTERM"); + +Console.CancelKeyPress += (sender, e) => +{ + e.Cancel = true; + Console.WriteLine("已接收 SIGINT (Ctrl+C)"); + tcs.SetResult(); + sigintReceived = true; +}; + +AssemblyLoadContext.Default.Unloading += ctx => +{ + if (!sigintReceived) + { + Console.WriteLine("已接收 SIGTERM"); + tcs.SetResult(); + } + else + { + Console.WriteLine("@AssemblyLoadContext.Default.Unloading,已處理 SIGINT,忽略 SIGTERM"); + } +}; + +AppDomain.CurrentDomain.ProcessExit += (sender, e) => +{ + if (!sigintReceived) + { + Console.WriteLine("已接收 SIGTERM"); + tcs.SetResult(); + } + else + { + Console.WriteLine("@AppDomain.CurrentDomain.ProcessExit,已處理 SIGINT,忽略 SIGTERM"); + } +}; + +await Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + // services.AddHostedService(); + services.AddHostedService(); + }) + .RunConsoleAsync(); +Console.WriteLine("下次再來唷~"); + diff --git a/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Properties/launchSettings.json b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Properties/launchSettings.json new file mode 100644 index 00000000..b665f30e --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Lab.GracefulShutdown.Net6": { + "commandName": "Project", + "environmentVariables": { + } + } + } +} diff --git a/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.sln b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.sln new file mode 100644 index 00000000..d979b64b --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.GracefulShutdown.Net6", "Lab.GracefulShutdown.Net6\Lab.GracefulShutdown.Net6.csproj", "{D21B2207-2D80-49B2-94A1-24234DBD9B8D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D21B2207-2D80-49B2-94A1-24234DBD9B8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D21B2207-2D80-49B2-94A1-24234DBD9B8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D21B2207-2D80-49B2-94A1-24234DBD9B8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D21B2207-2D80-49B2-94A1-24234DBD9B8D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Hangfire/Lab.HangfireEnqueueRetry/Lab.HangfireServer/packages.config b/Hangfire/Lab.HangfireEnqueueRetry/Lab.HangfireServer/packages.config index b0752a7e..dcd7fcac 100644 --- a/Hangfire/Lab.HangfireEnqueueRetry/Lab.HangfireServer/packages.config +++ b/Hangfire/Lab.HangfireEnqueueRetry/Lab.HangfireServer/packages.config @@ -13,7 +13,7 @@ - + diff --git a/Host/ConsoleAppNet48/LabHostedService.cs b/Host/ConsoleAppNet48/LabHostedService.cs deleted file mode 100644 index b580eb10..00000000 --- a/Host/ConsoleAppNet48/LabHostedService.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace ConsoleAppNet48 -{ - public class LabHostedService : IHostedService - { - private readonly ILogger _logger; - - public LabHostedService(ILogger logger, - IHostApplicationLifetime lifetime) - { - this._logger = logger; - - lifetime.ApplicationStarted.Register(this.OnStarted); - lifetime.ApplicationStopping.Register(this.OnStopping); - lifetime.ApplicationStopped.Register(this.OnStopped); - } - - public Task StartAsync(CancellationToken cancellationToken) - { - this._logger.LogInformation("1. StartAsync has been called."); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - this._logger.LogInformation("4. StopAsync has been called."); - - return Task.CompletedTask; - } - - private void OnStarted() - { - this._logger.LogInformation("2. OnStarted has been called."); - } - - private void OnStopped() - { - this._logger.LogInformation("5. OnStopped has been called."); - } - - private void OnStopping() - { - this._logger.LogInformation("3. OnStopping has been called."); - } - } -} \ No newline at end of file diff --git a/Host/Lab.Host.Env/.env b/Host/Lab.Host.Env/.env new file mode 100644 index 00000000..403721d0 --- /dev/null +++ b/Host/Lab.Host.Env/.env @@ -0,0 +1 @@ +ASPNETCORE_ENVIRONMENT=Staging \ No newline at end of file diff --git a/Host/Lab.Host.Env/.gitignore b/Host/Lab.Host.Env/.gitignore new file mode 100644 index 00000000..81c554f7 --- /dev/null +++ b/Host/Lab.Host.Env/.gitignore @@ -0,0 +1,350 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +#vistual studio extension +.localhistory + +# stress test output +stress/output + +# specflow feature.cs +**/*.feature.cs + +# secrets +secrets + +.DS_Store +*.zip + +deployments + +# minio local s3 +minio \ No newline at end of file diff --git a/Host/Lab.Host.Env/Taskfile.yml b/Host/Lab.Host.Env/Taskfile.yml new file mode 100644 index 00000000..e0166fd1 --- /dev/null +++ b/Host/Lab.Host.Env/Taskfile.yml @@ -0,0 +1,17 @@ +# Taskfile.yml + +version: "3" + +dotenv: [ "secrets/secrets.env" ] + +tasks: + webapi: + desc: WebApi Development + dir: "src/Lab.Host.Env.WebApi" + cmds: + - dotnet run --environment Staging + app: + desc: WebApi Development + dir: "src/Lab.Host.Env.ConsoleApp" + cmds: + - dotnet run --environment Production \ No newline at end of file diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/Lab.Host.Env.ConsoleApp.csproj b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/Lab.Host.Env.ConsoleApp.csproj new file mode 100644 index 00000000..100dd407 --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/Lab.Host.Env.ConsoleApp.csproj @@ -0,0 +1,37 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + true + PreserveNewest + PreserveNewest + + + true + PreserveNewest + PreserveNewest + + + true + PreserveNewest + PreserveNewest + + + true + PreserveNewest + PreserveNewest + + + + diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/Program.cs b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/Program.cs new file mode 100644 index 00000000..5f110bd6 --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/Program.cs @@ -0,0 +1,25 @@ +// See https://aka.ms/new-console-template for more information + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var hostBuilder = Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((hostContext, config) => + { + var environmentName = hostContext.HostingEnvironment.EnvironmentName; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + config.AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: true); + }) + ; +var host = hostBuilder.Build(); +var environment = host.Services.GetService(); +Console.WriteLine($"Environment: {environment.EnvironmentName}"); + +var configuration = host.Services.GetService(); +var version = configuration.GetSection("Extension:Version").Value; +Console.WriteLine($"Extension.Version: {version}"); + +await host.StartAsync(); +await host.StopAsync(); +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.Development.json b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.Development.json new file mode 100644 index 00000000..5fcf250c --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.Development.json @@ -0,0 +1,5 @@ +{ + "Extension": { + "Version": "Development" + } +} diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.Production.json b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.Production.json new file mode 100644 index 00000000..7d4ee65a --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.Production.json @@ -0,0 +1,5 @@ +{ + "Extension": { + "Version": "Production" + } +} diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.Staging.json b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.Staging.json new file mode 100644 index 00000000..d7c762b5 --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.Staging.json @@ -0,0 +1,5 @@ +{ + "Extension": { + "Version": "Staging" + } +} \ No newline at end of file diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.json b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.json new file mode 100644 index 00000000..fe523927 --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.ConsoleApp/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Extension": { + "Version": "Default" + } +} diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/Controllers/DemoController.cs b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/Controllers/DemoController.cs new file mode 100644 index 00000000..6449d88b --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/Controllers/DemoController.cs @@ -0,0 +1,28 @@ +using Lab.Host.Env.WebApi.ServiceModels; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.Host.Env.WebApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class DemoController : ControllerBase +{ + private IWebHostEnvironment _host; + private readonly ILogger _logger; + + public DemoController(ILogger logger, IWebHostEnvironment host) + { + this._logger = logger; + this._host = host; + } + + [HttpGet] + public async Task> Get(CancellationToken cancel = default) + { + return this.Ok(new + { + this._host.ApplicationName, + this._host.EnvironmentName + }); + } +} \ No newline at end of file diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/Lab.Host.Env.WebApi.csproj b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/Lab.Host.Env.WebApi.csproj new file mode 100644 index 00000000..b9baca3e --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/Lab.Host.Env.WebApi.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/Program.cs b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/Program.cs new file mode 100644 index 00000000..7e2863ed --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/Program.cs @@ -0,0 +1,38 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddControllers(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); + +builder.Services.AddSwaggerGen(); + +var environmentName = builder.Environment.EnvironmentName; +var configRoot = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: true) + .Build(); + +builder.Configuration.AddConfiguration(configRoot); +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +var version = app.Configuration.GetSection("Extension:Version").Value; +Console.WriteLine($"Environment: {app.Environment.EnvironmentName}"); +Console.WriteLine($"Extension.Version: {version}"); + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/ServiceModels/EnvironmentResponse.cs b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/ServiceModels/EnvironmentResponse.cs new file mode 100644 index 00000000..3c9d2e75 --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/ServiceModels/EnvironmentResponse.cs @@ -0,0 +1,5 @@ +namespace Lab.Host.Env.WebApi.ServiceModels; + +public class EnvironmentResponse +{ +} \ No newline at end of file diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.Development.json b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.Development.json new file mode 100644 index 00000000..5fcf250c --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.Development.json @@ -0,0 +1,5 @@ +{ + "Extension": { + "Version": "Development" + } +} diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.Production.json b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.Production.json new file mode 100644 index 00000000..7d4ee65a --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.Production.json @@ -0,0 +1,5 @@ +{ + "Extension": { + "Version": "Production" + } +} diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.Staging.json b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.Staging.json new file mode 100644 index 00000000..d7c762b5 --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.Staging.json @@ -0,0 +1,5 @@ +{ + "Extension": { + "Version": "Staging" + } +} \ No newline at end of file diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.json b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.json new file mode 100644 index 00000000..fe523927 --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.WebApi/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Extension": { + "Version": "Default" + } +} diff --git a/Host/Lab.Host.Env/src/Lab.Host.Env.sln b/Host/Lab.Host.Env/src/Lab.Host.Env.sln new file mode 100644 index 00000000..3a78e837 --- /dev/null +++ b/Host/Lab.Host.Env/src/Lab.Host.Env.sln @@ -0,0 +1,29 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Host.Env.ConsoleApp", "Lab.Host.Env.ConsoleApp\Lab.Host.Env.ConsoleApp.csproj", "{B7D0D873-72F9-455E-8012-2317E88A20A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Host.Env.WebApi", "Lab.Host.Env.WebApi\Lab.Host.Env.WebApi.csproj", "{C290A555-D176-45BF-B037-1BA36AE7F776}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{4601CB1B-259A-415E-B349-76EA4759821A}" + ProjectSection(SolutionItems) = preProject + ..\Taskfile.yml = ..\Taskfile.yml + ..\.env = ..\.env + ..\.gitignore = ..\.gitignore + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B7D0D873-72F9-455E-8012-2317E88A20A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7D0D873-72F9-455E-8012-2317E88A20A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7D0D873-72F9-455E-8012-2317E88A20A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7D0D873-72F9-455E-8012-2317E88A20A7}.Release|Any CPU.Build.0 = Release|Any CPU + {C290A555-D176-45BF-B037-1BA36AE7F776}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C290A555-D176-45BF-B037-1BA36AE7F776}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C290A555-D176-45BF-B037-1BA36AE7F776}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C290A555-D176-45BF-B037-1BA36AE7F776}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Host/Lab.MsHost.sln b/Host/Lab.MsHost.sln deleted file mode 100644 index ce7c2c7d..00000000 --- a/Host/Lab.MsHost.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppNet5", "ConsoleAppNet5\ConsoleAppNet5.csproj", "{1CE278B8-A6DB-422C-B3CD-151A1C5D3B4E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppNet48", "ConsoleAppNet48\ConsoleAppNet48.csproj", "{069BA841-E538-4E51-8D1D-F175107B1312}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1CE278B8-A6DB-422C-B3CD-151A1C5D3B4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1CE278B8-A6DB-422C-B3CD-151A1C5D3B4E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1CE278B8-A6DB-422C-B3CD-151A1C5D3B4E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1CE278B8-A6DB-422C-B3CD-151A1C5D3B4E}.Release|Any CPU.Build.0 = Release|Any CPU - {069BA841-E538-4E51-8D1D-F175107B1312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {069BA841-E538-4E51-8D1D-F175107B1312}.Debug|Any CPU.Build.0 = Debug|Any CPU - {069BA841-E538-4E51-8D1D-F175107B1312}.Release|Any CPU.ActiveCfg = Release|Any CPU - {069BA841-E538-4E51-8D1D-F175107B1312}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/Host/ConsoleAppNet48/AppHost.cs b/Host/Lab.MsHost/ConsoleAppNetFx48/AppHost.cs similarity index 97% rename from Host/ConsoleAppNet48/AppHost.cs rename to Host/Lab.MsHost/ConsoleAppNetFx48/AppHost.cs index 8e713810..aa205ec7 100644 --- a/Host/ConsoleAppNet48/AppHost.cs +++ b/Host/Lab.MsHost/ConsoleAppNetFx48/AppHost.cs @@ -4,12 +4,12 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace ConsoleAppNet48 +namespace ConsoleAppNetFx48 { public class AppHost : IHostedService { - private readonly ILogger logger; private readonly IHostApplicationLifetime appLifetime; + private readonly ILogger logger; public AppHost(ILogger logger, IHostApplicationLifetime appLifetime) { diff --git a/Host/ConsoleAppNet48/ConsoleAppNet48.csproj b/Host/Lab.MsHost/ConsoleAppNetFx48/ConsoleAppNetFx48.csproj similarity index 71% rename from Host/ConsoleAppNet48/ConsoleAppNet48.csproj rename to Host/Lab.MsHost/ConsoleAppNetFx48/ConsoleAppNetFx48.csproj index 50724ce9..aded3afa 100644 --- a/Host/ConsoleAppNet48/ConsoleAppNet48.csproj +++ b/Host/Lab.MsHost/ConsoleAppNetFx48/ConsoleAppNetFx48.csproj @@ -4,9 +4,8 @@ Exe net48 - - + diff --git a/Host/Lab.MsHost/ConsoleAppNetFx48/LabBackgroundService.cs b/Host/Lab.MsHost/ConsoleAppNetFx48/LabBackgroundService.cs new file mode 100644 index 00000000..56ea7423 --- /dev/null +++ b/Host/Lab.MsHost/ConsoleAppNetFx48/LabBackgroundService.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace ConsoleAppNetFx48 +{ + public class LabBackgroundService : BackgroundService + { + private readonly ILogger _logger; + + public LabBackgroundService(ILogger logger, + IHostApplicationLifetime appLifetime, + IHostLifetime hostLifetime, + IHostEnvironment hostEnvironment) + { + this._logger = logger; + appLifetime.ApplicationStarted.Register(this.OnStarted); + appLifetime.ApplicationStopping.Register(this.OnStopping); + appLifetime.ApplicationStopped.Register(this.OnStopped); + this._logger.LogInformation($"主機環境:" + + $"ApplicationName = {hostEnvironment.ApplicationName}\r\n" + + $"EnvironmentName = {hostEnvironment.EnvironmentName}\r\n" + + $"RootPath = {hostEnvironment.ContentRootPath}\r\n" + + $"Root File Provider = {hostEnvironment.ContentRootFileProvider}\r\n"); + } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + this._logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + await Task.Delay(1000, stoppingToken); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + this._logger.LogInformation("1. 調用 Host.StartAsync "); + return Task.CompletedTask; + } + private void OnStarted() + { + this._logger.LogInformation("2. 調用 OnStarted"); + } + private void OnStopping() + { + this._logger.LogInformation("3. 調用 OnStopping"); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + this._logger.LogInformation("4. 調用 Host.StopAsync"); + return Task.CompletedTask; + } + + private void OnStopped() + { + this._logger.LogInformation("5. 調用 OnStopped"); + } + } +} \ No newline at end of file diff --git a/Host/Lab.MsHost/ConsoleAppNetFx48/LabHostedService.cs b/Host/Lab.MsHost/ConsoleAppNetFx48/LabHostedService.cs new file mode 100644 index 00000000..78670489 --- /dev/null +++ b/Host/Lab.MsHost/ConsoleAppNetFx48/LabHostedService.cs @@ -0,0 +1,54 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace ConsoleAppNetFx48 +{ + public class LabHostedService : IHostedService + { + private readonly ILogger _logger; + + public LabHostedService(ILogger logger, + IHostApplicationLifetime appLifetime, + IHostLifetime hostLifetime, + IHostEnvironment hostEnvironment) + { + this._logger = logger; + appLifetime.ApplicationStarted.Register(this.OnStarted); + appLifetime.ApplicationStopping.Register(this.OnStopping); + appLifetime.ApplicationStopped.Register(this.OnStopped); + this._logger.LogInformation($"主機環境:" + + $"ApplicationName = {hostEnvironment.ApplicationName}\r\n" + + $"EnvironmentName = {hostEnvironment.EnvironmentName}\r\n" + + $"RootPath = {hostEnvironment.ContentRootPath}\r\n" + + $"Root File Provider = {hostEnvironment.ContentRootFileProvider}\r\n"); + + } + + public Task StartAsync(CancellationToken cancellationToken) + { + this._logger.LogInformation("1. 調用 Host.StartAsync "); + return Task.CompletedTask; + } + private void OnStarted() + { + this._logger.LogInformation("2. 調用 OnStarted"); + } + private void OnStopping() + { + this._logger.LogInformation("3. 調用 OnStopping"); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + this._logger.LogInformation("4. 調用 Host.StopAsync"); + return Task.CompletedTask; + } + + private void OnStopped() + { + this._logger.LogInformation("5. 調用 OnStopped"); + } + } +} \ No newline at end of file diff --git a/Host/Lab.MsHost/ConsoleAppNetFx48/Program.cs b/Host/Lab.MsHost/ConsoleAppNetFx48/Program.cs new file mode 100644 index 00000000..50d78f7d --- /dev/null +++ b/Host/Lab.MsHost/ConsoleAppNetFx48/Program.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace ConsoleAppNetFx48 +{ + // internal class Program1 + // { + // private static void Main(string[] args) + // { + // var hostBuilder = Host.CreateDefaultBuilder(args) + // .ConfigureServices((hostBuilder, services) => + // { + // services.AddHostedService(); + // Console.WriteLine($"注入 {nameof(LabHostedService)}"); + // }); + // var host = hostBuilder.Build(); + // host.RunAsync(); + // Console.WriteLine($"{nameof(LabHostedService)} 應用程式已啟動"); + // Console.ReadLine(); + // } + // } + + internal class Program + { + private static Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + var task = host.RunAsync(); + host.WaitForShutdownAsync(); + Console.WriteLine($"{nameof(LabHostedService)} 應用程式已啟動"); + + return task; + } + + private static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureServices((hostBuilder, services) => + { + services.AddHostedService(); + services.AddHostedService(); + Console.WriteLine("注入HostService"); + }) + ; + } + } +} \ No newline at end of file diff --git a/Host/Lab.MsHost/Lab.MsHost.sln b/Host/Lab.MsHost/Lab.MsHost.sln new file mode 100644 index 00000000..e2e6abce --- /dev/null +++ b/Host/Lab.MsHost/Lab.MsHost.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppNetFx48", "ConsoleAppNetFx48\ConsoleAppNetFx48.csproj", "{45E650BE-BBBF-4060-B2CB-049C45B6830D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {45E650BE-BBBF-4060-B2CB-049C45B6830D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45E650BE-BBBF-4060-B2CB-049C45B6830D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45E650BE-BBBF-4060-B2CB-049C45B6830D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45E650BE-BBBF-4060-B2CB-049C45B6830D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/CallSafeCreateService.bat b/Host/Lab.WorkerService/ConsoleAppNetFx48/CallSafeCreateService.bat new file mode 100644 index 00000000..8e500b2b --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/CallSafeCreateService.bat @@ -0,0 +1,15 @@ +@echo off +set batchFolder=%~dp0 +set serviceName=ConsoleAppNetFx48 +set serviceDisplayName=ConsoleAppNetFx48 +set serviceDescription="" +set serviceLaunchPath=%batchFolder%bin\ConsoleAppNetFx48.exe +set serviceLogonId=.\setup +set serviceLogonPassword=password +::set serverName=\\Computer Name +set serverName= +Call SafeStopService %serviceName% %serverName% +Call SafeDeleteService %serviceName% %serverName% +Call SafeCreateService %serviceName% %serviceDisplayName% %serviceDescription% %serviceLaunchPath% %serviceLogonId% %serviceLogonPassword% %serverName% +Call SafeStartService %serviceName% %serverName% + diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/ConsoleAppNetFx48.csproj b/Host/Lab.WorkerService/ConsoleAppNetFx48/ConsoleAppNetFx48.csproj new file mode 100644 index 00000000..eacddb05 --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/ConsoleAppNetFx48.csproj @@ -0,0 +1,49 @@ + + + + net48 + bin + bin\ConsoleAppNetFx48.xml + dotnet-ConsoleAppNetFx48-525DDA0C-18EF-4AE3-A405-A9653AA2D910 + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + true + Always + PreserveNewest + + + true + Always + PreserveNewest + + + diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/DoThing.cs b/Host/Lab.WorkerService/ConsoleAppNetFx48/DoThing.cs new file mode 100644 index 00000000..a5347b9f --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/DoThing.cs @@ -0,0 +1,38 @@ +// using System; +// using System.Timers; +// using NLog; +// +// namespace ConsoleAppNetFx48 +// { +// public class DoThing +// { +// private static readonly ILogger s_logger; +// private readonly Timer _timer; +// +// static DoThing() +// { +// if (s_logger == null) +// { +// s_logger = LogManager.GetCurrentClassLogger(); +// } +// } +// +// public DoThing() +// { +// this._timer = new Timer(1000) {AutoReset = true}; +// this._timer.Elapsed += (sender, eventArgs) => Console.WriteLine($"Now Time:{DateTime.Now}"); +// } +// +// public void Start() +// { +// this._timer.Start(); +// s_logger.Trace("Timer Start"); +// } +// +// public void Stop() +// { +// this._timer.Stop(); +// s_logger.Trace("Timer Stop"); +// } +// } +// } \ No newline at end of file diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/Player.cs b/Host/Lab.WorkerService/ConsoleAppNetFx48/Player.cs new file mode 100644 index 00000000..8969ad99 --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/Player.cs @@ -0,0 +1,9 @@ +namespace ConsoleAppNetFx48 +{ + public struct Player + { + public string AppId { get; set; } + + public string Key { get; set; } + } +} \ No newline at end of file diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/Program.cs b/Host/Lab.WorkerService/ConsoleAppNetFx48/Program.cs new file mode 100644 index 00000000..814cb9a2 --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/Program.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Topshelf; +using Topshelf.Configuration; +using Topshelf.Extensions.Hosting; +using Host = Microsoft.Extensions.Hosting.Host; + +namespace ConsoleAppNetFx48 +{ + public class Program + { + private static void Main(string[] args) + { + var hostBuilder = CreateHostBuilder(args); + + var exitCode = + hostBuilder.RunAsTopshelfService(config => + { + // var assemblyName = Assembly.GetEntryAssembly().GetName().Name; + // config.SetServiceName(assemblyName); + // config.SetDisplayName(assemblyName); + // config.SetDescription("Runs a generic host as a Topshelf service."); + // config.RunAsPrompt(); + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }); + config.UseLoggingExtensions(loggerFactory); + + var configRoot = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + var topshelfSection = configRoot.GetSection("Topshelf"); + config.ApplyConfiguration(topshelfSection); + }); + Console.WriteLine($"服務控制狀態:{exitCode}"); + + // hostBuilder.Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + + // .UseWindowsService() + .ConfigureServices((hostContext, services) => { services.AddHostedService(); }); + } + } +} \ No newline at end of file diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/Properties/launchSettings.json b/Host/Lab.WorkerService/ConsoleAppNetFx48/Properties/launchSettings.json new file mode 100644 index 00000000..b1932b1c --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "ConsoleAppNetFx48": { + "commandName": "Project", + "dotnetRunMessages": "true", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeCreateService.bat b/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeCreateService.bat new file mode 100644 index 00000000..5851866c --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeCreateService.bat @@ -0,0 +1,116 @@ +@echo off + +IF [%1]==[] GOTO usage +IF [%2]==[] GOTO usage +IF [%3]==[] GOTO usage +IF [%4]==[] GOTO usage +IF [%5]==[] GOTO usage + +IF NOT "%1"=="" SET serviceName=%1 +IF NOT "%2"=="" SET serviceDisplayName=%2 +IF NOT "%3"=="" SET serviceDescription=%3 +IF NOT "%4"=="" SET serviceLaunchPath=%4 +IF NOT "%5"=="" SET serviceLogonId=%5 +IF NOT "%6"=="" SET serviceLogonPassword=%6 +IF NOT "%7"=="" SET serverName=%7 + +SC %serverName% query %serviceName% + +IF errorlevel 1060 GOTO ServiceNotFound +IF errorlevel 1722 GOTO SystemOffline +IF errorlevel 1001 GOTO DeletingServiceDelay + +:ResolveInitialState +SC %serverName% query %serviceName% | FIND "STATE" | FIND "RUNNING" + +IF errorlevel 0 IF NOT errorlevel 1 GOTO StopService + +SC %serverName% query %serviceName% | FIND "STATE" | FIND "STOPPED" + +IF errorlevel 0 IF NOT errorlevel 1 GOTO StoppedService + +SC %serverName% query %serviceName% | FIND "STATE" | FIND "PAUSED" + +IF errorlevel 0 IF NOT errorlevel 1 GOTO SystemOffline + +echo Service State is changing, waiting for service to resolve its state before making changes + +sc %serverName% query %serviceName% | Find "STATE" +ping -n 2 127.0.0.1 > NUL +GOTO ResolveInitialState + +:StopService +echo Stopping %serviceName% on %serverName% +sc %serverName% stop %serviceName% +GOTO StoppingService + +:StoppingServiceDelay +echo Waiting for %serviceName% to stop +ping -n 2 127.0.0.1 > NUL + +:StoppingService +SC %serverName% query %serviceName% | FIND "STATE" | FIND "STOPPED" +IF errorlevel 1 GOTO StoppingServiceDelay + +:StoppedService +echo %serviceName% on %serverName% is stopped +GOTO DeleteService + +:DeleteService +echo Deleting %serviceName% on %serverName% +SC %serverName% delete %serviceName% + +:DeletingServiceDelay +echo Waiting for %serviceName% to get deleted +ping -n 2 127.0.0.1 > NUL + +:DeletingService +SC %serverName% query %serviceName% +IF NOT errorlevel 1060 GOTO DeletingServiceDelay + +:DeletedService +echo %serviceName% on %serverName% is deleted +GOTO CreateService + +:SystemOffline +echo Server %serverName% is not accessible or is offline +GOTO End + +:ServiceNotFound +echo Service %serviceName% is not installed on Server %serverName% +GOTO CreateService + +:CreateService +echo Creating %serviceName% on %serverName% +::SC %serverName% create %serviceName% binpath= "%serviceLaunchPath%" displayname= "THS MSMQ %serviceDisplayName% Agent" +SC %serverName% create %serviceName% binpath= "%serviceLaunchPath%" +SC %serverName% config %serviceName% displayname= "%serviceDisplayName%" +SC %serverName% config %serviceName% obj= %serviceLogonId% password= "%serviceLogonPassword%" +SC %serverName% config %serviceName% start= auto +SC %serverName% description %serviceName% "%serviceDescription%" +::SC "%serverName%" config "%serviceName%" type= share start= auto + +:CreatingServiceDelay +echo Waiting for %serviceName% to get created +ping -n 2 127.0.0.1 > NUL + +:CreatingService +::SC %serverName% query %serviceName% >NUL +SC %serverName% query %serviceName% | FIND "STATE" | FIND "STOPPED" +IF errorlevel 1 GOTO CreatingServiceDelay + +:CreatedService +echo %serviceName% on %serverName% is created +GOTO End + +:usage +echo Will cause a local/remote service to START (if not already started). +echo This script will waiting for the service to enter the started state if necessary. +echo. +echo %0 [service name] [system name] +echo Example: %0 MyService server1 +echo Example: %0 MyService (for local PC) +echo. + +::GOTO:eof +:End \ No newline at end of file diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeDeleteService.bat b/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeDeleteService.bat new file mode 100644 index 00000000..1e045a82 --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeDeleteService.bat @@ -0,0 +1,77 @@ +@echo off + +IF [%1]==[] GOTO usage +IF NOT "%1"=="" SET serviceName=%1 +IF NOT "%2"=="" SET serverName=%2 + +SC %serverName% query %serviceName% +IF errorlevel 1060 GOTO ServiceNotFound +IF errorlevel 1722 GOTO SystemOffline +IF errorlevel 1001 GOTO DeletingServiceDelay + +:ResolveInitialState +SC %serverName% query %serviceName% | FIND "STATE" | FIND "RUNNING" +IF errorlevel 0 IF NOT errorlevel 1 GOTO StopService + +SC %serverName% query %serviceName% | FIND "STATE" | FIND "STOPPED" +IF errorlevel 0 IF NOT errorlevel 1 GOTO StoppedService + +SC %serverName% query %serviceName% | FIND "STATE" | FIND "PAUSED" +IF errorlevel 0 IF NOT errorlevel 1 GOTO SystemOffline +echo Service State is changing, waiting for service to resolve its state before making changes + +SC %serverName% query %serviceName% | Find "STATE" +ping -n 2 127.0.0.1 > NUL +GOTO ResolveInitialState + +:StopService +echo Stopping %serviceName% on %serverName% +SC %serverName% stop %serviceName% + +GOTO StoppingService +:StoppingServiceDelay +echo Waiting for %serviceName% to stop +ping -n 2 127.0.0.1 > NUL + +:StoppingService +SC %serverName% query %serviceName% | FIND "STATE" | FIND "STOPPED" +IF errorlevel 1 GOTO StoppingServiceDelay + +:StoppedService +echo %serviceName% on %serverName% is stopped +GOTO DeleteService + +:DeleteService +SC %serverName% delete %serviceName% + +:DeletingServiceDelay +echo Waiting for %serviceName% to get deleted +ping -n 2 127.0.0.1 > NUL + +:DeletingService +SC %serverName% query %serviceName% +IF NOT errorlevel 1060 GOTO DeletingServiceDelay + +:DeletedService +echo %serviceName% on %serverName% is deleted +GOTO End + +:SystemOffline +echo Server %serverName% is not accessible or is offline +GOTO End + +:ServiceNotFound +echo Service %serviceName% is not installed on Server %serverName% +::exit /b 0 +GOTO End + +:usage +echo Will cause a local/remote service to START (if not already started). +echo This script will waiting for the service to enter the started state if necessary. +echo. +echo %0 [service name] [system name] +echo Example: %0 MyService server1 +echo Example: %0 MyService (for local PC) +echo. + +:End diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeStartService.bat b/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeStartService.bat new file mode 100644 index 00000000..98bdf838 --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeStartService.bat @@ -0,0 +1,65 @@ +@echo off + +IF [%1]==[] GOTO usage +IF NOT "%1"=="" SET serviceName=%1 +IF NOT "%2"=="" SET serverName=%2 + +SC %serverName% query %serviceName% +IF errorlevel 1060 GOTO ServiceNotFound +IF errorlevel 1722 GOTO SystemOffline + +:ResolveInitialState + +SC %serverName% query %serviceName% | FIND "STATE" | FIND "STOPPED" +IF errorlevel 0 IF NOT errorlevel 1 GOTO StartService + +SC %serverName% query %serviceName% | FIND "STATE" | FIND "RUNNING" +IF errorlevel 0 IF NOT errorlevel 1 GOTO StartedService + +SC %serverName% query %serviceName% | FIND "STATE" | FIND "PAUSED" +IF errorlevel 0 IF NOT errorlevel 1 GOTO SystemOffline +echo Service State is changing, waiting for service to resolve its state before making changes + +SC %serverName% query %serviceName% | Find "STATE" >NUL +ping -n 2 127.0.0.1 > NUL + +GOTO ResolveInitialState + +:StartService +echo Starting %serviceName% on %serverName% +SC %serverName% start %serviceName% + +GOTO StartingService + +:StartingServiceDelay +echo Waiting for %serviceName% to start +ping -n 2 127.0.0.1 > NUL + +:StartingService +SC %serverName% query %serviceName% | FIND "STATE" | FIND "RUNNING" +IF errorlevel 1 GOTO StartingServiceDelay + +:StartedService +echo %serviceName% on %serverName% is started +GOTO End + +:SystemOffline +echo Server %serverName% is not accessible or is offline +GOTO End + +:ServiceNotFound +echo Service %serviceName% is not installed on Server %serverName% +::exit /b 0 +GOTO End + +:usage +echo Will cause a local/remote service to START (if not already started). +echo This script will waiting for the service to enter the started state if necessary. +echo. +echo %0 [service name] [system name] +echo Example: %0 MyService server1 +echo Example: %0 MyService (for local PC) +echo. + +::GOTO:eof +:End \ No newline at end of file diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeStopService.bat b/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeStopService.bat new file mode 100644 index 00000000..952915f2 --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/SafeStopService.bat @@ -0,0 +1,64 @@ +@echo off + +IF [%1]==[] GOTO usage +IF NOT "%1"=="" SET serviceName=%1 +IF NOT "%2"=="" SET serverName=%2 + +SC %serverName% query %serviceName% +IF errorlevel 1060 GOTO ServiceNotFound +IF errorlevel 1722 GOTO SystemOffline + +:ResolveInitialState +SC %serverName% query %serviceName% | FIND "STATE" | FIND "RUNNING" +IF errorlevel 0 IF NOT errorlevel 1 GOTO StopService + +SC %serverName% query %serviceName% | FIND "STATE" | FIND "STOPPED" +IF errorlevel 0 IF NOT errorlevel 1 GOTO StoppedService + +SC %serverName% query %serviceName% | FIND "STATE" | FIND "PAUSED" +IF errorlevel 0 IF NOT errorlevel 1 GOTO SystemOffline +echo Service State is changing, waiting for service to resolve its state before making changes + +SC %serverName% query %serviceName% | Find "STATE" +ping -n 2 127.0.0.1 > NUL +GOTO ResolveInitialState + +:StopService +echo Stopping %serviceName% on %serverName% +SC %serverName% stop %serviceName% +GOTO StoppingService + +:StoppingServiceDelay +echo Waiting for %serviceName% to stop +ping -n 2 127.0.0.1 > NUL + +:StoppingService +SC %serverName% query %serviceName% | FIND "STATE" | FIND "STOPPED" +IF errorlevel 1 GOTO StoppingServiceDelay + +:StoppedService +echo %serviceName% on %serverName% is stopped +GOTO End + +:SystemOffline +echo Server %serverName% is not accessible or is offline +GOTO End + +:ServiceNotFound +echo Service %serviceName% is not installed on Server %serverName% +::exit /b 0 +GOTO End + +:usage +echo Will cause a local/remote service to STOP (if not already stopped). +echo This script will waiting for the service to enter the stopped state if necessary. +echo. +echo %0 [service name] [system name] {reason} +echo Example: %0 MyService server1 {reason} +echo Example: %0 MyService (for local PC, DO NOT specify reason) +echo. +echo For reason codes, run "sc stop" + + +::GOTO:eof +:End \ No newline at end of file diff --git a/Host/ConsoleAppNet48/LabBackgroundService.cs b/Host/Lab.WorkerService/ConsoleAppNetFx48/Worker.cs similarity index 60% rename from Host/ConsoleAppNet48/LabBackgroundService.cs rename to Host/Lab.WorkerService/ConsoleAppNetFx48/Worker.cs index ccca500a..5609eb04 100644 --- a/Host/ConsoleAppNet48/LabBackgroundService.cs +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/Worker.cs @@ -4,13 +4,16 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace ConsoleAppNet48 +namespace ConsoleAppNetFx48 { - public class LabBackgroundService : BackgroundService + public class Worker : BackgroundService { - private readonly ILogger _logger; + private readonly ILogger _logger; - public LabBackgroundService(ILogger logger) + public Worker(ILogger logger, + IHostApplicationLifetime appLifetime, + IHostLifetime hostLifetime, + IHostEnvironment hostEnvironment) { this._logger = logger; } diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/appsettings.Development.json b/Host/Lab.WorkerService/ConsoleAppNetFx48/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Host/Lab.WorkerService/ConsoleAppNetFx48/appsettings.json b/Host/Lab.WorkerService/ConsoleAppNetFx48/appsettings.json new file mode 100644 index 00000000..b8026813 --- /dev/null +++ b/Host/Lab.WorkerService/ConsoleAppNetFx48/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Topshelf": { + "ServiceName": "ConsoleAppNetFx48", + "DisplayName": "ConsoleAppNetFx48", + "Description":"Runs a generic host as a Topshelf service.", + "Instance":"1", + "Account":{ + "Username":".\\setup", + "Password":"password" + }, + "StopTimeout":"60" + } +} diff --git a/Host/Lab.WorkerService/Lab.WorkerService.sln b/Host/Lab.WorkerService/Lab.WorkerService.sln new file mode 100644 index 00000000..80856a97 --- /dev/null +++ b/Host/Lab.WorkerService/Lab.WorkerService.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppNetFx48", "ConsoleAppNetFx48\ConsoleAppNetFx48.csproj", "{6E527E3E-6180-4250-B61D-0B69208737C8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6E527E3E-6180-4250-B61D-0B69208737C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E527E3E-6180-4250-B61D-0B69208737C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E527E3E-6180-4250-B61D-0B69208737C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E527E3E-6180-4250-B61D-0B69208737C8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Json/Lab.JsonCompare/Lab.Json.UnitTest/Lab.Json.UnitTest.csproj b/Json/Lab.JsonCompare/Lab.Json.UnitTest/Lab.Json.UnitTest.csproj new file mode 100644 index 00000000..c83c50db --- /dev/null +++ b/Json/Lab.JsonCompare/Lab.Json.UnitTest/Lab.Json.UnitTest.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + + false + + + + + + + + + + diff --git a/Json/Lab.JsonCompare/Lab.Json.UnitTest/UnitTest1.cs b/Json/Lab.JsonCompare/Lab.Json.UnitTest/UnitTest1.cs new file mode 100644 index 00000000..9d646e0a --- /dev/null +++ b/Json/Lab.JsonCompare/Lab.Json.UnitTest/UnitTest1.cs @@ -0,0 +1,12 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.Json.UnitTest; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void TestMethod1() + { + } +} \ No newline at end of file diff --git a/Json/Lab.JsonCompare/Lab.JsonCompare.UnitTest/Lab.JsonCompare.UnitTest.csproj b/Json/Lab.JsonCompare/Lab.JsonCompare.UnitTest/Lab.JsonCompare.UnitTest.csproj new file mode 100644 index 00000000..fbe32aae --- /dev/null +++ b/Json/Lab.JsonCompare/Lab.JsonCompare.UnitTest/Lab.JsonCompare.UnitTest.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + diff --git a/Json/Lab.JsonCompare/Lab.JsonCompare.UnitTest/NewtonsoftJsonDiffPathTests.cs b/Json/Lab.JsonCompare/Lab.JsonCompare.UnitTest/NewtonsoftJsonDiffPathTests.cs new file mode 100644 index 00000000..4a12ad38 --- /dev/null +++ b/Json/Lab.JsonCompare/Lab.JsonCompare.UnitTest/NewtonsoftJsonDiffPathTests.cs @@ -0,0 +1,57 @@ +using System; +using JsonDiffPatchDotNet; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; + +namespace Lab.JsonCompare.UnitTest; + +[TestClass] +public class NewtonsoftJsonDiffPathTests +{ + [TestMethod] + public void 比對兩個一樣的JObject() + { + var source = new JObject + { + { "Integer", 12345 }, + { "String", "A string" }, + { "Items", new JArray(1, 2) } + }; + + var dest = new JObject + { + { "Integer", 12345 }, + { "String", "A string" }, + { "Items", new JArray(1, 2) } + }; + var isEquals = JToken.DeepEquals(source, dest); + Assert.IsTrue(isEquals); + } + + [TestMethod] + public void 比對兩個不一樣的JObject() + { + var source = new JObject + { + { "Integer", 12345 }, + { "String", "A string" }, + { "Items", new JArray(1, 2) } + }; + + var dest = new JObject + { + { "integer", 12345 }, + { "String", "A string" }, + { "Items", new JArray(1, 2, new JArray { "a", "b" }) } + }; + var diffPath = new JsonDiffPatch(); + var diff = diffPath.Diff(source, dest); + + if (diff != null) + { + Console.WriteLine(diff.ToString()); + } + + Assert.IsNotNull(diff); + } +} \ No newline at end of file diff --git a/Json/Lab.JsonCompare/Lab.JsonCompare.UnitTest/SystemTextJsonDiffPathTests.cs b/Json/Lab.JsonCompare/Lab.JsonCompare.UnitTest/SystemTextJsonDiffPathTests.cs new file mode 100644 index 00000000..32259e38 --- /dev/null +++ b/Json/Lab.JsonCompare/Lab.JsonCompare.UnitTest/SystemTextJsonDiffPathTests.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Text.Json.JsonDiffPatch; +using System.Text.Json.JsonDiffPatch.MsTest; +using System.Text.Json.Nodes; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.JsonCompare.UnitTest; + +[TestClass] +public class SystemTextJsonDiffPathTests +{ + [TestMethod] + public void 比對兩個一樣的Json字串_via_JsonDiffPatcher() + { + var source = new JsonObject + { + { "Integer", 12345 }, + { "String", "A string" }, + { "Items", new JsonArray(1, 2) } + }; + + var dest = new JsonObject + { + { "Integer", 12345 }, + { "String", "A string" }, + { "Items", new JsonArray(1, 2) } + }; + + var left = source.ToJsonString(); + var right = dest.ToJsonString(); + var diff = JsonDiffPatcher.Diff(left, right); + if (diff != null) + { + Console.WriteLine(JsonSerializer.Serialize(diff)); + } + + Assert.IsNull(diff); + } + + [TestMethod] + public void 比對兩個不一樣的JsonObject() + { + var source = new JsonObject + { + { "Integer", 12345 }, + { "String", JsonValue.Create("A string") }, + { "Items", new JsonArray(1, 2) } + }; + + var dest = new JsonObject + { + { "integer", 12345 }, + { "String", "A string" }, + { "Items", new JsonArray(1, 2, new JsonArray { "a", "b" }) } + }; + + var diff = source.Diff(dest); + if (diff != null) + { + Console.WriteLine(JsonSerializer.Serialize(diff)); + } + + Assert.IsNotNull(diff); + } + + [TestMethod] + public void 比對兩個不一樣的JsonNode() + { + var source = new JsonObject + { + { "Integer", 12345 }, + { "String", JsonValue.Create("A string") }, + { "Items", new JsonArray(1, 2) } + }; + + var dest = new JsonObject + { + { "integer", 12345 }, + { "String", "A string" }, + { "Items", new JsonArray(1, 2, new JsonArray { "a", "b" }) } + }; + + var left = JsonNode.Parse(source.ToJsonString()); + var right = JsonNode.Parse(dest.ToJsonString()); + var diff = left.Diff(right); + if (diff != null) + { + Console.WriteLine(JsonSerializer.Serialize(diff)); + } + + Assert.IsNotNull(diff); + } + + [TestMethod] + public void 比對兩個不一樣的JsonObject_via_JsonAssert() + { + var source = new JsonObject + { + { "Integer", 12345 }, + { "String", JsonValue.Create("A string") }, + { "Items", new JsonArray(1, 2) } + }; + + var dest = new JsonObject + { + { "integer", 12345 }, + { "String", "A string" }, + { "Items", new JsonArray(1, 2, new JsonArray { "a", "b" }) } + }; + Assert.That.JsonAreEqual(source, dest, true); + } + + [TestMethod] + public void 比對兩個不一樣的JsonDocument() + { + var source = new JsonObject + { + { "Integer", 12345 }, + { "String", JsonValue.Create("A string") }, + { "Items", new JsonArray(1, 2) } + }; + + var dest = new JsonObject + { + { "Integer", 12345 }, + { "String", JsonValue.Create("A string") }, + { "Items", new JsonArray(1, 2, new JsonArray { "a", "b" }) } + }; + + var left = JsonDocument.Parse(source.ToJsonString()); + var right = JsonDocument.Parse(dest.ToJsonString()); + + var isEquals = left.DeepEquals(right); + Assert.IsFalse(isEquals); + } + + [TestMethod] + public void 更新節點() + { + var source = new JsonObject + { + { "Integer", 12345 }, + { "String", "A string" }, + { "Items", new JsonArray(1, 2, new JsonArray { "a", "b" }) } + }; + + var dest = new JsonObject + { + { "Integer", 12345 }, + { "String", "A string" }, + { "Items", new JsonArray(1, 2) } + }; + + var left = JsonNode.Parse(dest.ToJsonString()); + var right = JsonNode.Parse(source.ToJsonString()); + + //左邊不等於來源,跟我認知的不一樣 + var diff = left.Diff(right); + JsonDiffPatcher.Patch(ref left, diff); + + Assert.That.JsonAreEqual(right, right, true); + } +} \ No newline at end of file diff --git a/Json/Lab.JsonCompare/Lab.JsonCompare.sln b/Json/Lab.JsonCompare/Lab.JsonCompare.sln new file mode 100644 index 00000000..77f11d88 --- /dev/null +++ b/Json/Lab.JsonCompare/Lab.JsonCompare.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.JsonCompare.UnitTest", "Lab.JsonCompare.UnitTest\Lab.JsonCompare.UnitTest.csproj", "{D79E4743-D20C-4712-8E8E-F0323D05A339}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D79E4743-D20C-4712-8E8E-F0323D05A339}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D79E4743-D20C-4712-8E8E-F0323D05A339}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D79E4743-D20C-4712-8E8E-F0323D05A339}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D79E4743-D20C-4712-8E8E-F0323D05A339}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Json/Lab.JsonConverter/Lab.JsonConverter.sln b/Json/Lab.JsonConverter/Lab.JsonConverter.sln new file mode 100644 index 00000000..07e7f922 --- /dev/null +++ b/Json/Lab.JsonConverter/Lab.JsonConverter.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.JsonConverterLib", "Lab.JsonConverterLib\Lab.JsonConverterLib.csproj", "{5E5A6E58-2175-435A-AC74-617DBF7A70F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.JsonConverterLib.UnitTest", "Lab.JsonConverterLib.UnitTest\Lab.JsonConverterLib.UnitTest.csproj", "{E36B4A45-46F6-4709-ACF6-2887F4D26B5D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5E5A6E58-2175-435A-AC74-617DBF7A70F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E5A6E58-2175-435A-AC74-617DBF7A70F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E5A6E58-2175-435A-AC74-617DBF7A70F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E5A6E58-2175-435A-AC74-617DBF7A70F2}.Release|Any CPU.Build.0 = Release|Any CPU + {E36B4A45-46F6-4709-ACF6-2887F4D26B5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E36B4A45-46F6-4709-ACF6-2887F4D26B5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E36B4A45-46F6-4709-ACF6-2887F4D26B5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E36B4A45-46F6-4709-ACF6-2887F4D26B5D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Json/Lab.JsonConverter/Lab.JsonConverterLib.UnitTest/DictionaryStringObjectJsonConverter2Tests.cs b/Json/Lab.JsonConverter/Lab.JsonConverterLib.UnitTest/DictionaryStringObjectJsonConverter2Tests.cs new file mode 100644 index 00000000..12b8ebaf --- /dev/null +++ b/Json/Lab.JsonConverter/Lab.JsonConverterLib.UnitTest/DictionaryStringObjectJsonConverter2Tests.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.JsonConverterLib.UnitTest; + +[TestClass] +public class DictionaryStringObjectJsonConverter2Tests +{ + [TestMethod] + public void JsonDocument轉Dictionary() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter2() } + }; + var expected = CreateDictionary(); + var json = JsonSerializer.Serialize(expected); + + using var jsonDoc = json.ToJsonDocument(); + var actual = jsonDoc.To>(options); + AssertThat(actual, expected); + } + + [TestMethod] + public void JsonsNode轉Dictionary() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter2() } + }; + var expected = CreateDictionary(); + var json = JsonSerializer.Serialize(expected); + + var jsonObject = json.ToJsonNode(); + var actual = jsonObject.To>(options); + + AssertThat(actual, expected); + } + + private static void AssertThat(Dictionary actual, Dictionary expected) + { + actual["model"].Should().BeEquivalentTo(expected["model"]); + actual["decimalArray"].Should().BeEquivalentTo(expected["decimalArray"]); + Assert.AreEqual(expected["dateTimeOffset"], actual["dateTimeOffset"]); + Assert.AreEqual(expected["decimal"], actual["decimal"]); + Assert.AreEqual(expected["guid"], actual["guid"]); + Assert.AreEqual(expected["string"], actual["string"]); + } + + [TestMethod] + public void Memory轉Dictionary() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var expected = CreateDictionary(); + + var json = JsonSerializer.Serialize(expected, options); + var jsonMemory = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + var actual = JsonSerializer.Deserialize>(jsonMemory, options); + + AssertThat(actual, expected); + } + + [TestMethod] + public void 字串轉Dictionary() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var expected = CreateDictionary(); + + var json = JsonSerializer.Serialize(expected, options); + var actual = JsonSerializer.Deserialize>(json, options); + + AssertThat(actual, expected); + } + + [TestMethod] + public void 字串轉Dictionary_失敗() + { + var expected = new Dictionary + { + ["i"] = 255, + ["s"] = "字串", + ["d"] = new DateTime(1900, 1, 1), + ["a"] = new[] { 1, 2 }, + ["o"] = new { Prop = 123 } + }; + var json = JsonSerializer.Serialize(expected); + + var actual = JsonSerializer.Deserialize>(json); + Assert.AreNotEqual(expected["i"], actual["i"]); + Assert.AreNotEqual(expected["s"], actual["s"]); + + // 反序列化之後得到 JsonElement Type,必須要要透過其他手段才能取得真實的值 + Assert.AreEqual("JsonElement", actual["s"].GetType().Name); + Assert.AreEqual(expected["i"], ((JsonElement)actual["i"]).GetInt32()); + Assert.AreEqual(expected["s"], ((JsonElement)actual["s"]).GetString()); + } + + private static Dictionary CreateDictionary() + { + var expected = new Dictionary + { + ["model"] = new Dictionary + { + { "Age", 19 }, + { "Name", "小章" } + }, + ["decimalArray"] = new List { 1, (decimal)2.1 }, + ["dateTimeOffset"] = DateTimeOffset.Now, + ["decimal"] = (decimal)3.1416, + ["guid"] = Guid.NewGuid(), + ["string"] = "字串", + }; + return expected; + } + + private class Model + { + public string Name { get; set; } + + public int Age { get; set; } + } +} \ No newline at end of file diff --git a/Json/Lab.JsonConverter/Lab.JsonConverterLib.UnitTest/DictionaryStringObjectJsonConverterTests.cs b/Json/Lab.JsonConverter/Lab.JsonConverterLib.UnitTest/DictionaryStringObjectJsonConverterTests.cs new file mode 100644 index 00000000..4cd6e579 --- /dev/null +++ b/Json/Lab.JsonConverter/Lab.JsonConverterLib.UnitTest/DictionaryStringObjectJsonConverterTests.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.JsonConverterLib.UnitTest; + +[TestClass] +public class DictionaryStringObjectJsonConverterTests +{ + [TestMethod] + public void JsonDocument轉Dictionary() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var expected = CreateDictionary(); + var json = JsonSerializer.Serialize(expected); + + using var jsonDoc = json.ToJsonDocument(); + var actual = jsonDoc.To>(options); + + AssertThat(expected, actual); + } + + [TestMethod] + public void JsonDocument轉Dictionary1() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var expected = CreateDictionary(); + + using var jsonDoc = expected.ToJsonDocument(); + var actual = jsonDoc.To>(options); + AssertThat(expected, actual); + } + + [TestMethod] + public void JsonsNode轉Dictionary() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var expected = CreateDictionary(); + var json = JsonSerializer.Serialize(expected); + + var jsonObject = json.ToJsonNode(); + var actual = jsonObject.To>(options); + + AssertThat(expected, actual); + } + + [TestMethod] + public void Memory轉Dictionary() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + + var expected = CreateDictionary(); + + var json = JsonSerializer.Serialize(expected, options); + var jsonMemory = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + var actual = JsonSerializer.Deserialize>(jsonMemory, options); + AssertThat(expected, actual); + } + + [TestMethod] + public void 字串轉Dictionary() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var expected = CreateDictionary(); + + var json = JsonSerializer.Serialize(expected, options); + var actual = JsonSerializer.Deserialize>(json, options); + AssertThat(expected, actual); + } + + [TestMethod] + public void 字串轉Dictionary_失敗() + { + var expected = new Dictionary + { + ["i"] = 255, + ["s"] = "字串", + ["d"] = new DateTime(1900, 1, 1), + ["a"] = new[] { 1, 2 }, + ["o"] = new { Prop = 123 } + }; + var json = JsonSerializer.Serialize(expected); + + var actual = JsonSerializer.Deserialize>(json); + Assert.AreNotEqual(expected["i"], actual["i"]); + Assert.AreNotEqual(expected["s"], actual["s"]); + + // 反序列化之後得到 JsonElement Type,必須要要透過其他手段才能取得真實的值 + Assert.AreEqual("JsonElement", actual["s"].GetType().Name); + Assert.AreEqual(expected["i"], ((JsonElement)actual["i"]).GetInt32()); + Assert.AreEqual(expected["s"], ((JsonElement)actual["s"]).GetString()); + } + + private static void AssertAnonymousType(Dictionary actual) + { + var expected = new Dictionary + { + { "Prop", (long)123 } + }; + + Assert.AreEqual(expected["Prop"], actual["Prop"]); + } + + private static void AssertDecimalArray(List actual) + { + var expected = new List + { + (long)1, + (decimal)2.1 + }; + + Assert.AreEqual(expected[0], actual[0]); + Assert.AreEqual(expected[1], actual[1]); + } + + private static void AssertThat(Dictionary expected, Dictionary actual) + { + Assert.AreEqual(expected["dateTimeOffset"], actual["dateTimeOffset"]); + Assert.AreEqual(expected["string"], actual["string"]); + Assert.AreEqual(expected["long"], actual["long"]); + Assert.AreEqual(expected["decimal"], actual["decimal"]); + Assert.AreEqual(expected["null"], actual["null"]); + + AssertAnonymousType(actual["anonymousType"] as Dictionary); + AssertDecimalArray(actual["decimalArray"] as List); + } + + private static Dictionary CreateDictionary() + { + var expected = new Dictionary + { + ["anonymousType"] = new { Prop = 123 }, + ["model"] = new Model { Age = 19, Name = "小章" }, + ["null"] = null!, + ["dateTimeOffset"] = DateTimeOffset.Now, + ["long"] = (long)255, + ["decimal"] = (decimal)3.1416, + ["guid"] = Guid.NewGuid(), + ["string"] = "字串", + ["decimalArray"] = new[] { 1, (decimal)2.1 }, + }; + return expected; + } + + private class Model + { + public string Name { get; set; } + + public int Age { get; set; } + } +} \ No newline at end of file diff --git a/Json/Lab.JsonConverter/Lab.JsonConverterLib.UnitTest/Lab.JsonConverterLib.UnitTest.csproj b/Json/Lab.JsonConverter/Lab.JsonConverterLib.UnitTest/Lab.JsonConverterLib.UnitTest.csproj new file mode 100644 index 00000000..ec415e09 --- /dev/null +++ b/Json/Lab.JsonConverter/Lab.JsonConverterLib.UnitTest/Lab.JsonConverterLib.UnitTest.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + diff --git a/Json/Lab.JsonConverter/Lab.JsonConverterLib/DictionaryStringObjectJsonConverter.cs b/Json/Lab.JsonConverter/Lab.JsonConverterLib/DictionaryStringObjectJsonConverter.cs new file mode 100644 index 00000000..28ecf719 --- /dev/null +++ b/Json/Lab.JsonConverter/Lab.JsonConverterLib/DictionaryStringObjectJsonConverter.cs @@ -0,0 +1,114 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lab.JsonConverterLib; + +public class DictionaryStringObjectJsonConverter : JsonConverter> +{ + public override Dictionary Read(ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException($"JsonTokenType was of type {reader.TokenType}, only objects are supported"); + } + + var results = new Dictionary(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return results; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException("JsonTokenType was not PropertyName"); + } + + var propertyName = reader.GetString(); + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new JsonException("Failed to get property name"); + } + + reader.Read(); + + results.Add(propertyName, this.ReadValue(ref reader, options)); + } + + return results; + } + + public override void Write(Utf8JsonWriter writer, + Dictionary value, + JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (var key in value.Keys) + { + WriteValue(writer, key, value[key], options); + } + + writer.WriteEndObject(); + } + + private object ReadValue(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + if (reader.TryGetDateTimeOffset(out var dateOffset)) + { + return dateOffset; + } + + if (reader.TryGetGuid(out var guid)) + { + return guid; + } + + return reader.GetString(); + case JsonTokenType.False: + case JsonTokenType.True: + return reader.GetBoolean(); + case JsonTokenType.Null: + return null; + case JsonTokenType.Number: + if (reader.TryGetInt64(out var result)) + { + return result; + } + + return reader.GetDecimal(); + case JsonTokenType.StartObject: + return this.Read(ref reader, null, options); + case JsonTokenType.StartArray: + var list = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + list.Add(this.ReadValue(ref reader, options)); + } + + return list; + default: + throw new JsonException($"'{reader.TokenType}' is not supported"); + } + } + + private static void WriteValue(Utf8JsonWriter writer, + string key, + object value, + JsonSerializerOptions options) + { + if (key != null) + { + writer.WritePropertyName(key); + } + + JsonSerializer.Serialize(writer, value, options); + } +} \ No newline at end of file diff --git a/Json/Lab.JsonConverter/Lab.JsonConverterLib/DictionaryStringObjectJsonConverter2.cs b/Json/Lab.JsonConverter/Lab.JsonConverterLib/DictionaryStringObjectJsonConverter2.cs new file mode 100644 index 00000000..9de6b135 --- /dev/null +++ b/Json/Lab.JsonConverter/Lab.JsonConverterLib/DictionaryStringObjectJsonConverter2.cs @@ -0,0 +1,145 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lab.JsonConverterLib; + +public class DictionaryStringObjectJsonConverter2 : JsonConverter> +{ + public override Dictionary Read(ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException($"JsonTokenType was of type {reader.TokenType}, only objects are supported"); + } + + var results = new Dictionary(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return results; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException("JsonTokenType was not PropertyName"); + } + + var propertyName = reader.GetString(); + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new JsonException("Failed to get property name"); + } + + reader.Read(); + + results.Add(propertyName, this.ReadValue(ref reader, options)); + } + + return results; + } + + public override void Write(Utf8JsonWriter writer, + Dictionary value, + JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (var key in value.Keys) + { + WriteValue(writer, key, value[key], options); + } + + writer.WriteEndObject(); + } + + private Dictionary ReadObjectValue(ref Utf8JsonReader reader, + JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException($"JsonTokenType was of type {reader.TokenType}, only objects are supported"); + } + + var results = new Dictionary(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return results; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException("JsonTokenType was not PropertyName"); + } + + var propertyName = reader.GetString(); + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new JsonException("Failed to get property name"); + } + + reader.Read(); + + results.Add(propertyName, this.ReadValue(ref reader, options)); + } + + return results; + } + + private object ReadValue(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + if (reader.TryGetDateTimeOffset(out var dateOffset)) + { + return dateOffset; + } + + if (reader.TryGetGuid(out var guid)) + { + return guid; + } + + return reader.GetString(); + case JsonTokenType.False: + case JsonTokenType.True: + return reader.GetBoolean(); + case JsonTokenType.Null: + return null; + case JsonTokenType.Number: + return reader.GetDecimal(); + case JsonTokenType.StartObject: + return this.ReadObjectValue(ref reader, options); + case JsonTokenType.StartArray: + var list = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + list.Add(this.ReadValue(ref reader, options)); + } + + return list; + default: + throw new JsonException($"'{reader.TokenType}' is not supported"); + } + } + + private static void WriteValue(Utf8JsonWriter writer, + string key, + object value, + JsonSerializerOptions options) + { + if (key != null) + { + writer.WritePropertyName(key); + } + + JsonSerializer.Serialize(writer, value, options); + } +} \ No newline at end of file diff --git a/Json/Lab.JsonConverter/Lab.JsonConverterLib/JsonDocumentExtensions.cs b/Json/Lab.JsonConverter/Lab.JsonConverterLib/JsonDocumentExtensions.cs new file mode 100644 index 00000000..c97c67eb --- /dev/null +++ b/Json/Lab.JsonConverter/Lab.JsonConverterLib/JsonDocumentExtensions.cs @@ -0,0 +1,41 @@ +using System.Text; +using System.Text.Json; + +namespace Lab.JsonConverterLib; + +public static class JsonDocumentExtensions +{ + public static T To(this JsonDocument source, + JsonSerializerOptions options = default) + { + return source.Deserialize(options); + } + + public static JsonDocument ToJsonDocument(this T source, + JsonDocumentOptions options = default) + where T : class + { + return JsonDocument.Parse(JsonSerializer.SerializeToUtf8Bytes(source), options); + } + + public static JsonDocument ToJsonDocument(this string source, + JsonDocumentOptions options = default) + { + return JsonDocument.Parse(source, options); + } + + public static string ToJsonString(this JsonDocument source, + JsonWriterOptions options = default) + { + if (source == null) + { + return null; + } + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream, options); + source.WriteTo(writer); + writer.Flush(); + return Encoding.UTF8.GetString(stream.ToArray()); + } +} \ No newline at end of file diff --git a/Json/Lab.JsonConverter/Lab.JsonConverterLib/JsonNodeExtensions.cs b/Json/Lab.JsonConverter/Lab.JsonConverterLib/JsonNodeExtensions.cs new file mode 100644 index 00000000..cb7593d4 --- /dev/null +++ b/Json/Lab.JsonConverter/Lab.JsonConverterLib/JsonNodeExtensions.cs @@ -0,0 +1,42 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Lab.JsonConverterLib; + +public static class JsonNodeExtensions +{ + public static T To(this JsonNode source, + JsonSerializerOptions options = default) + { + return source.Deserialize(options); + } + + public static JsonNode ToJsonNode(this T source, + JsonNodeOptions options = default) + where T : class + { + return JsonNode.Parse(JsonSerializer.SerializeToUtf8Bytes(source), options); + } + + public static JsonNode ToJsonNode(this string source, + JsonNodeOptions options = default) + { + return JsonNode.Parse(source, options); + } + + public static string ToJsonString(this JsonNode source, + JsonWriterOptions options = default) + { + if (source == null) + { + return null; + } + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream, options); + source.WriteTo(writer); + writer.Flush(); + return Encoding.UTF8.GetString(stream.ToArray()); + } +} \ No newline at end of file diff --git a/Json/Lab.JsonConverter/Lab.JsonConverterLib/Lab.JsonConverterLib.csproj b/Json/Lab.JsonConverter/Lab.JsonConverterLib/Lab.JsonConverterLib.csproj new file mode 100644 index 00000000..eb2460e9 --- /dev/null +++ b/Json/Lab.JsonConverter/Lab.JsonConverterLib/Lab.JsonConverterLib.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/ILineNotifyProvider.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/ILineNotifyProvider.cs new file mode 100644 index 00000000..3cb72d37 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/ILineNotifyProvider.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using Lab.LineBot.SDK.Models; + +namespace Lab.LineBot.SDK +{ + public interface ILineNotifyProvider + { + string CreateAuthorizeCodeUrl(AuthorizeCodeUrlRequest request); + + Task GetAccessTokenAsync(TokenRequest request, CancellationToken cancelToken); + + Task GetAccessTokenInfoAsync(string accessToken, CancellationToken cancelToken); + + Task NotifyAsync(NotifyWithStickerRequest request, CancellationToken cancelToken); + + Task NotifyAsync(NotifyWithImageRequest request, CancellationToken cancelToken); + + Task RevokeAsync(string accessToken, CancellationToken cancelToken); + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/Internals/MimeTypeMapping.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/Internals/MimeTypeMapping.cs new file mode 100644 index 00000000..f4dd853d --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/Internals/MimeTypeMapping.cs @@ -0,0 +1,827 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lab.LineBot.SDK.Internals +{ + /// + /// Class MimeTypeMap. + /// + public static class MimeTypeMapping + { + private const string Dot = "."; + private const string QuestionMark = "?"; + private const string DefaultMimeType = "application/octet-stream"; + + private static readonly Lazy> _mappings = + new Lazy>(BuildMappings); + + /// + /// Gets the extension from the provided MINE type. + /// + /// Type of the MIME. + /// if set to true, throws error if extension's not found. + /// The extension. + /// + /// + public static string GetExtension(string mimeType, bool throwErrorIfNotFound = true) + { + if (mimeType == null) + { + throw new ArgumentNullException(nameof(mimeType)); + } + + if (mimeType.StartsWith(Dot)) + { + throw new ArgumentException("Requested mime type is not valid: " + mimeType); + } + + if (_mappings.Value.TryGetValue(mimeType, out var extension)) + { + return extension; + } + + if (throwErrorIfNotFound) + { + throw new ArgumentException("Requested mime type is not registered: " + mimeType); + } + + return string.Empty; + } + + /// + /// Gets the type of the MIME from the provided string. + /// + /// The filename or extension. + /// The MIME type. + /// + public static string GetMimeType(string str) + { + return TryGetMimeType(str, out var result) ? result : DefaultMimeType; + } + + /// + /// Tries to get the type of the MIME from the provided string. + /// + /// The filename or extension. + /// The variable to store the MIME type. + /// The MIME type. + /// + public static bool TryGetMimeType(string str, out string mimeType) + { + if (str == null) + { + throw new ArgumentNullException(nameof(str)); + } + + var indexQuestionMark = str.IndexOf(QuestionMark, StringComparison.Ordinal); + if (indexQuestionMark != -1) + { + str = str.Remove(indexQuestionMark); + } + + if (!str.StartsWith(Dot)) + { + var index = str.LastIndexOf(Dot); + if (index != -1 && str.Length > index + 1) + { + str = str.Substring(index + 1); + } + + str = Dot + str; + } + + return _mappings.Value.TryGetValue(str, out mimeType); + } + + private static IDictionary BuildMappings() + { + var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + #region Big freaking list of mime types + + // maps both ways, + // extension -> mime type + // and + // mime type -> extension + // + // any mime types on left side not pre-loaded on right side, are added automatically + // some mime types can map to multiple extensions, so to get a deterministic mapping, + // add those to the dictionary specifically + // + // combination of values from Windows 7 Registry and + // from C:\Windows\System32\inetsrv\config\applicationHost.config + // some added, including .7z and .dat + // + // Some added based on http://www.iana.org/assignments/media-types/media-types.xhtml + // which lists mime types, but not extensions + // + {".323", "text/h323"}, + {".3g2", "video/3gpp2"}, + {".3gp", "video/3gpp"}, + {".3gp2", "video/3gpp2"}, + {".3gpp", "video/3gpp"}, + {".7z", "application/x-7z-compressed"}, + {".aa", "audio/audible"}, + {".AAC", "audio/aac"}, + {".aaf", "application/octet-stream"}, + {".aax", "audio/vnd.audible.aax"}, + {".ac3", "audio/ac3"}, + {".aca", "application/octet-stream"}, + {".accda", "application/msaccess.addin"}, + {".accdb", "application/msaccess"}, + {".accdc", "application/msaccess.cab"}, + {".accde", "application/msaccess"}, + {".accdr", "application/msaccess.runtime"}, + {".accdt", "application/msaccess"}, + {".accdw", "application/msaccess.webapplication"}, + {".accft", "application/msaccess.ftemplate"}, + {".acx", "application/internet-property-stream"}, + {".AddIn", "text/xml"}, + {".ade", "application/msaccess"}, + {".adobebridge", "application/x-bridge-url"}, + {".adp", "application/msaccess"}, + {".ADT", "audio/vnd.dlna.adts"}, + {".ADTS", "audio/aac"}, + {".afm", "application/octet-stream"}, + {".ai", "application/postscript"}, + {".aif", "audio/aiff"}, + {".aifc", "audio/aiff"}, + {".aiff", "audio/aiff"}, + {".air", "application/vnd.adobe.air-application-installer-package+zip"}, + {".amc", "application/mpeg"}, + {".anx", "application/annodex"}, + {".apk", "application/vnd.android.package-archive"}, + {".apng", "image/apng"}, + {".application", "application/x-ms-application"}, + {".art", "image/x-jg"}, + {".asa", "application/xml"}, + {".asax", "application/xml"}, + {".ascx", "application/xml"}, + {".asd", "application/octet-stream"}, + {".asf", "video/x-ms-asf"}, + {".ashx", "application/xml"}, + {".asi", "application/octet-stream"}, + {".asm", "text/plain"}, + {".asmx", "application/xml"}, + {".aspx", "application/xml"}, + {".asr", "video/x-ms-asf"}, + {".asx", "video/x-ms-asf"}, + {".atom", "application/atom+xml"}, + {".au", "audio/basic"}, + {".avci", "image/avci"}, + {".avcs", "image/avcs"}, + {".avi", "video/x-msvideo"}, + {".avif", "image/avif"}, + {".avifs", "image/avif-sequence"}, + {".axa", "audio/annodex"}, + {".axs", "application/olescript"}, + {".axv", "video/annodex"}, + {".bas", "text/plain"}, + {".bcpio", "application/x-bcpio"}, + {".bin", "application/octet-stream"}, + {".bmp", "image/bmp"}, + {".c", "text/plain"}, + {".cab", "application/octet-stream"}, + {".caf", "audio/x-caf"}, + {".calx", "application/vnd.ms-office.calx"}, + {".cat", "application/vnd.ms-pki.seccat"}, + {".cc", "text/plain"}, + {".cd", "text/plain"}, + {".cdda", "audio/aiff"}, + {".cdf", "application/x-cdf"}, + {".cer", "application/x-x509-ca-cert"}, + {".cfg", "text/plain"}, + {".chm", "application/octet-stream"}, + {".class", "application/x-java-applet"}, + {".clp", "application/x-msclip"}, + {".cmd", "text/plain"}, + {".cmx", "image/x-cmx"}, + {".cnf", "text/plain"}, + {".cod", "image/cis-cod"}, + {".config", "application/xml"}, + {".contact", "text/x-ms-contact"}, + {".coverage", "application/xml"}, + {".cpio", "application/x-cpio"}, + {".cpp", "text/plain"}, + {".crd", "application/x-mscardfile"}, + {".crl", "application/pkix-crl"}, + {".crt", "application/x-x509-ca-cert"}, + {".cs", "text/plain"}, + {".csdproj", "text/plain"}, + {".csh", "application/x-csh"}, + {".csproj", "text/plain"}, + {".css", "text/css"}, + {".csv", "text/csv"}, + {".cur", "application/octet-stream"}, + {".czx", "application/x-czx"}, + {".cxx", "text/plain"}, + {".dat", "application/octet-stream"}, + {".datasource", "application/xml"}, + {".dbproj", "text/plain"}, + {".dcr", "application/x-director"}, + {".def", "text/plain"}, + {".deploy", "application/octet-stream"}, + {".der", "application/x-x509-ca-cert"}, + {".dgml", "application/xml"}, + {".dib", "image/bmp"}, + {".dif", "video/x-dv"}, + {".dir", "application/x-director"}, + {".disco", "text/xml"}, + {".divx", "video/divx"}, + {".dll", "application/x-msdownload"}, + {".dll.config", "text/xml"}, + {".dlm", "text/dlm"}, + {".doc", "application/msword"}, + {".docm", "application/vnd.ms-word.document.macroEnabled.12"}, + {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {".dot", "application/msword"}, + {".dotm", "application/vnd.ms-word.template.macroEnabled.12"}, + {".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, + {".dsp", "application/octet-stream"}, + {".dsw", "text/plain"}, + {".dtd", "text/xml"}, + {".dtsConfig", "text/xml"}, + {".dv", "video/x-dv"}, + {".dvi", "application/x-dvi"}, + {".dwf", "drawing/x-dwf"}, + {".dwg", "application/acad"}, + {".dwp", "application/octet-stream"}, + {".dxf", "application/x-dxf"}, + {".dxr", "application/x-director"}, + {".eml", "message/rfc822"}, + {".emf", "image/emf"}, + {".emz", "application/octet-stream"}, + {".eot", "application/vnd.ms-fontobject"}, + {".eps", "application/postscript"}, + {".es", "application/ecmascript"}, + {".etl", "application/etl"}, + {".etx", "text/x-setext"}, + {".evy", "application/envoy"}, + {".exe", "application/vnd.microsoft.portable-executable"}, + {".exe.config", "text/xml"}, + {".f4v", "video/mp4"}, + {".fdf", "application/vnd.fdf"}, + {".fif", "application/fractals"}, + {".filters", "application/xml"}, + {".fla", "application/octet-stream"}, + {".flac", "audio/flac"}, + {".flr", "x-world/x-vrml"}, + {".flv", "video/x-flv"}, + {".fsscript", "application/fsharp-script"}, + {".fsx", "application/fsharp-script"}, + {".generictest", "application/xml"}, + {".geojson", "application/geo+json"}, + {".gif", "image/gif"}, + {".gpx", "application/gpx+xml"}, + {".group", "text/x-ms-group"}, + {".gsm", "audio/x-gsm"}, + {".gtar", "application/x-gtar"}, + {".gz", "application/x-gzip"}, + {".h", "text/plain"}, + {".hdf", "application/x-hdf"}, + {".hdml", "text/x-hdml"}, + {".heic", "image/heic"}, + {".heics", "image/heic-sequence"}, + {".heif", "image/heif"}, + {".heifs", "image/heif-sequence"}, + {".hhc", "application/x-oleobject"}, + {".hhk", "application/octet-stream"}, + {".hhp", "application/octet-stream"}, + {".hlp", "application/winhlp"}, + {".hpp", "text/plain"}, + {".hqx", "application/mac-binhex40"}, + {".hta", "application/hta"}, + {".htc", "text/x-component"}, + {".htm", "text/html"}, + {".html", "text/html"}, + {".htt", "text/webviewhtml"}, + {".hxa", "application/xml"}, + {".hxc", "application/xml"}, + {".hxd", "application/octet-stream"}, + {".hxe", "application/xml"}, + {".hxf", "application/xml"}, + {".hxh", "application/octet-stream"}, + {".hxi", "application/octet-stream"}, + {".hxk", "application/xml"}, + {".hxq", "application/octet-stream"}, + {".hxr", "application/octet-stream"}, + {".hxs", "application/octet-stream"}, + {".hxt", "text/html"}, + {".hxv", "application/xml"}, + {".hxw", "application/octet-stream"}, + {".hxx", "text/plain"}, + {".i", "text/plain"}, + {".ical", "text/calendar"}, + {".icalendar", "text/calendar"}, + {".ico", "image/x-icon"}, + {".ics", "text/calendar"}, + {".idl", "text/plain"}, + {".ief", "image/ief"}, + {".ifb", "text/calendar"}, + {".iii", "application/x-iphone"}, + {".inc", "text/plain"}, + {".inf", "application/octet-stream"}, + {".ini", "text/plain"}, + {".inl", "text/plain"}, + {".ins", "application/x-internet-signup"}, + {".ipa", "application/x-itunes-ipa"}, + {".ipg", "application/x-itunes-ipg"}, + {".ipproj", "text/plain"}, + {".ipsw", "application/x-itunes-ipsw"}, + {".iqy", "text/x-ms-iqy"}, + {".isp", "application/x-internet-signup"}, + {".isma", "application/octet-stream"}, + {".ismv", "application/octet-stream"}, + {".ite", "application/x-itunes-ite"}, + {".itlp", "application/x-itunes-itlp"}, + {".itms", "application/x-itunes-itms"}, + {".itpc", "application/x-itunes-itpc"}, + {".IVF", "video/x-ivf"}, + {".jar", "application/java-archive"}, + {".java", "application/octet-stream"}, + {".jck", "application/liquidmotion"}, + {".jcz", "application/liquidmotion"}, + {".jfif", "image/pjpeg"}, + {".jnlp", "application/x-java-jnlp-file"}, + {".jpb", "application/octet-stream"}, + {".jpe", "image/jpeg"}, + {".jpeg", "image/jpeg"}, + {".jpg", "image/jpeg"}, + {".js", "application/javascript"}, + {".json", "application/json"}, + {".jsx", "text/jscript"}, + {".jsxbin", "text/plain"}, + {".latex", "application/x-latex"}, + {".library-ms", "application/windows-library+xml"}, + {".lit", "application/x-ms-reader"}, + {".loadtest", "application/xml"}, + {".lpk", "application/octet-stream"}, + {".lsf", "video/x-la-asf"}, + {".lst", "text/plain"}, + {".lsx", "video/x-la-asf"}, + {".lzh", "application/octet-stream"}, + {".m13", "application/x-msmediaview"}, + {".m14", "application/x-msmediaview"}, + {".m1v", "video/mpeg"}, + {".m2t", "video/vnd.dlna.mpeg-tts"}, + {".m2ts", "video/vnd.dlna.mpeg-tts"}, + {".m2v", "video/mpeg"}, + {".m3u", "audio/x-mpegurl"}, + {".m3u8", "audio/x-mpegurl"}, + {".m4a", "audio/m4a"}, + {".m4b", "audio/m4b"}, + {".m4p", "audio/m4p"}, + {".m4r", "audio/x-m4r"}, + {".m4v", "video/x-m4v"}, + {".mac", "image/x-macpaint"}, + {".mak", "text/plain"}, + {".man", "application/x-troff-man"}, + {".manifest", "application/x-ms-manifest"}, + {".map", "text/plain"}, + {".master", "application/xml"}, + {".mbox", "application/mbox"}, + {".mda", "application/msaccess"}, + {".mdb", "application/x-msaccess"}, + {".mde", "application/msaccess"}, + {".mdp", "application/octet-stream"}, + {".me", "application/x-troff-me"}, + {".mfp", "application/x-shockwave-flash"}, + {".mht", "message/rfc822"}, + {".mhtml", "message/rfc822"}, + {".mid", "audio/mid"}, + {".midi", "audio/mid"}, + {".mix", "application/octet-stream"}, + {".mk", "text/plain"}, + {".mk3d", "video/x-matroska-3d"}, + {".mka", "audio/x-matroska"}, + {".mkv", "video/x-matroska"}, + {".mmf", "application/x-smaf"}, + {".mno", "text/xml"}, + {".mny", "application/x-msmoney"}, + {".mod", "video/mpeg"}, + {".mov", "video/quicktime"}, + {".movie", "video/x-sgi-movie"}, + {".mp2", "video/mpeg"}, + {".mp2v", "video/mpeg"}, + {".mp3", "audio/mpeg"}, + {".mp4", "video/mp4"}, + {".mp4v", "video/mp4"}, + {".mpa", "video/mpeg"}, + {".mpe", "video/mpeg"}, + {".mpeg", "video/mpeg"}, + {".mpf", "application/vnd.ms-mediapackage"}, + {".mpg", "video/mpeg"}, + {".mpp", "application/vnd.ms-project"}, + {".mpv2", "video/mpeg"}, + {".mqv", "video/quicktime"}, + {".ms", "application/x-troff-ms"}, + {".msg", "application/vnd.ms-outlook"}, + {".msi", "application/octet-stream"}, + {".mso", "application/octet-stream"}, + {".mts", "video/vnd.dlna.mpeg-tts"}, + {".mtx", "application/xml"}, + {".mvb", "application/x-msmediaview"}, + {".mvc", "application/x-miva-compiled"}, + {".mxf", "application/mxf"}, + {".mxp", "application/x-mmxp"}, + {".nc", "application/x-netcdf"}, + {".nsc", "video/x-ms-asf"}, + {".nws", "message/rfc822"}, + {".ocx", "application/octet-stream"}, + {".oda", "application/oda"}, + {".odb", "application/vnd.oasis.opendocument.database"}, + {".odc", "application/vnd.oasis.opendocument.chart"}, + {".odf", "application/vnd.oasis.opendocument.formula"}, + {".odg", "application/vnd.oasis.opendocument.graphics"}, + {".odh", "text/plain"}, + {".odi", "application/vnd.oasis.opendocument.image"}, + {".odl", "text/plain"}, + {".odm", "application/vnd.oasis.opendocument.text-master"}, + {".odp", "application/vnd.oasis.opendocument.presentation"}, + {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {".odt", "application/vnd.oasis.opendocument.text"}, + {".oga", "audio/ogg"}, + {".ogg", "audio/ogg"}, + {".ogv", "video/ogg"}, + {".ogx", "application/ogg"}, + {".one", "application/onenote"}, + {".onea", "application/onenote"}, + {".onepkg", "application/onenote"}, + {".onetmp", "application/onenote"}, + {".onetoc", "application/onenote"}, + {".onetoc2", "application/onenote"}, + {".opus", "audio/ogg"}, + {".orderedtest", "application/xml"}, + {".osdx", "application/opensearchdescription+xml"}, + {".otf", "application/font-sfnt"}, + {".otg", "application/vnd.oasis.opendocument.graphics-template"}, + {".oth", "application/vnd.oasis.opendocument.text-web"}, + {".otp", "application/vnd.oasis.opendocument.presentation-template"}, + {".ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, + {".ott", "application/vnd.oasis.opendocument.text-template"}, + {".oxps", "application/oxps"}, + {".oxt", "application/vnd.openofficeorg.extension"}, + {".p10", "application/pkcs10"}, + {".p12", "application/x-pkcs12"}, + {".p7b", "application/x-pkcs7-certificates"}, + {".p7c", "application/pkcs7-mime"}, + {".p7m", "application/pkcs7-mime"}, + {".p7r", "application/x-pkcs7-certreqresp"}, + {".p7s", "application/pkcs7-signature"}, + {".pbm", "image/x-portable-bitmap"}, + {".pcast", "application/x-podcast"}, + {".pct", "image/pict"}, + {".pcx", "application/octet-stream"}, + {".pcz", "application/octet-stream"}, + {".pdf", "application/pdf"}, + {".pfb", "application/octet-stream"}, + {".pfm", "application/octet-stream"}, + {".pfx", "application/x-pkcs12"}, + {".pgm", "image/x-portable-graymap"}, + {".pic", "image/pict"}, + {".pict", "image/pict"}, + {".pkgdef", "text/plain"}, + {".pkgundef", "text/plain"}, + {".pko", "application/vnd.ms-pki.pko"}, + {".pls", "audio/scpls"}, + {".pma", "application/x-perfmon"}, + {".pmc", "application/x-perfmon"}, + {".pml", "application/x-perfmon"}, + {".pmr", "application/x-perfmon"}, + {".pmw", "application/x-perfmon"}, + {".png", "image/png"}, + {".pnm", "image/x-portable-anymap"}, + {".pnt", "image/x-macpaint"}, + {".pntg", "image/x-macpaint"}, + {".pnz", "image/png"}, + {".pot", "application/vnd.ms-powerpoint"}, + {".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"}, + {".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, + {".ppa", "application/vnd.ms-powerpoint"}, + {".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"}, + {".ppm", "image/x-portable-pixmap"}, + {".pps", "application/vnd.ms-powerpoint"}, + {".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"}, + {".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, + {".ppt", "application/vnd.ms-powerpoint"}, + {".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"}, + {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {".prf", "application/pics-rules"}, + {".prm", "application/octet-stream"}, + {".prx", "application/octet-stream"}, + {".ps", "application/postscript"}, + {".psc1", "application/PowerShell"}, + {".psd", "application/octet-stream"}, + {".psess", "application/xml"}, + {".psm", "application/octet-stream"}, + {".psp", "application/octet-stream"}, + {".pst", "application/vnd.ms-outlook"}, + {".pub", "application/x-mspublisher"}, + {".pwz", "application/vnd.ms-powerpoint"}, + {".qht", "text/x-html-insertion"}, + {".qhtm", "text/x-html-insertion"}, + {".qt", "video/quicktime"}, + {".qti", "image/x-quicktime"}, + {".qtif", "image/x-quicktime"}, + {".qtl", "application/x-quicktimeplayer"}, + {".qxd", "application/octet-stream"}, + {".ra", "audio/x-pn-realaudio"}, + {".ram", "audio/x-pn-realaudio"}, + {".rar", "application/x-rar-compressed"}, + {".ras", "image/x-cmu-raster"}, + {".rat", "application/rat-file"}, + {".rc", "text/plain"}, + {".rc2", "text/plain"}, + {".rct", "text/plain"}, + {".rdlc", "application/xml"}, + {".reg", "text/plain"}, + {".resx", "application/xml"}, + {".rf", "image/vnd.rn-realflash"}, + {".rgb", "image/x-rgb"}, + {".rgs", "text/plain"}, + {".rm", "application/vnd.rn-realmedia"}, + {".rmi", "audio/mid"}, + {".rmp", "application/vnd.rn-rn_music_package"}, + {".rmvb", "application/vnd.rn-realmedia-vbr"}, + {".roff", "application/x-troff"}, + {".rpm", "audio/x-pn-realaudio-plugin"}, + {".rqy", "text/x-ms-rqy"}, + {".rtf", "application/rtf"}, + {".rtx", "text/richtext"}, + {".rvt", "application/octet-stream"}, + {".ruleset", "application/xml"}, + {".s", "text/plain"}, + {".safariextz", "application/x-safari-safariextz"}, + {".scd", "application/x-msschedule"}, + {".scr", "text/plain"}, + {".sct", "text/scriptlet"}, + {".sd2", "audio/x-sd2"}, + {".sdp", "application/sdp"}, + {".sea", "application/octet-stream"}, + {".searchConnector-ms", "application/windows-search-connector+xml"}, + {".setpay", "application/set-payment-initiation"}, + {".setreg", "application/set-registration-initiation"}, + {".settings", "application/xml"}, + {".sgimb", "application/x-sgimb"}, + {".sgml", "text/sgml"}, + {".sh", "application/x-sh"}, + {".shar", "application/x-shar"}, + {".shtml", "text/html"}, + {".sit", "application/x-stuffit"}, + {".sitemap", "application/xml"}, + {".skin", "application/xml"}, + {".skp", "application/x-koan"}, + {".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"}, + {".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, + {".slk", "application/vnd.ms-excel"}, + {".sln", "text/plain"}, + {".slupkg-ms", "application/x-ms-license"}, + {".smd", "audio/x-smd"}, + {".smi", "application/octet-stream"}, + {".smx", "audio/x-smd"}, + {".smz", "audio/x-smd"}, + {".snd", "audio/basic"}, + {".snippet", "application/xml"}, + {".snp", "application/octet-stream"}, + {".sql", "application/sql"}, + {".sol", "text/plain"}, + {".sor", "text/plain"}, + {".spc", "application/x-pkcs7-certificates"}, + {".spl", "application/futuresplash"}, + {".spx", "audio/ogg"}, + {".src", "application/x-wais-source"}, + {".srf", "text/plain"}, + {".SSISDeploymentManifest", "text/xml"}, + {".ssm", "application/streamingmedia"}, + {".sst", "application/vnd.ms-pki.certstore"}, + {".stl", "application/vnd.ms-pki.stl"}, + {".sv4cpio", "application/x-sv4cpio"}, + {".sv4crc", "application/x-sv4crc"}, + {".svc", "application/xml"}, + {".svg", "image/svg+xml"}, + {".swf", "application/x-shockwave-flash"}, + {".step", "application/step"}, + {".stp", "application/step"}, + {".t", "application/x-troff"}, + {".tar", "application/x-tar"}, + {".tcl", "application/x-tcl"}, + {".testrunconfig", "application/xml"}, + {".testsettings", "application/xml"}, + {".tex", "application/x-tex"}, + {".texi", "application/x-texinfo"}, + {".texinfo", "application/x-texinfo"}, + {".tgz", "application/x-compressed"}, + {".thmx", "application/vnd.ms-officetheme"}, + {".thn", "application/octet-stream"}, + {".tif", "image/tiff"}, + {".tiff", "image/tiff"}, + {".tlh", "text/plain"}, + {".tli", "text/plain"}, + {".toc", "application/octet-stream"}, + {".tr", "application/x-troff"}, + {".trm", "application/x-msterminal"}, + {".trx", "application/xml"}, + {".ts", "video/vnd.dlna.mpeg-tts"}, + {".tsv", "text/tab-separated-values"}, + {".ttf", "application/font-sfnt"}, + {".tts", "video/vnd.dlna.mpeg-tts"}, + {".txt", "text/plain"}, + {".u32", "application/octet-stream"}, + {".uls", "text/iuls"}, + {".user", "text/plain"}, + {".ustar", "application/x-ustar"}, + {".vb", "text/plain"}, + {".vbdproj", "text/plain"}, + {".vbk", "video/mpeg"}, + {".vbproj", "text/plain"}, + {".vbs", "text/vbscript"}, + {".vcf", "text/x-vcard"}, + {".vcproj", "application/xml"}, + {".vcs", "text/plain"}, + {".vcxproj", "application/xml"}, + {".vddproj", "text/plain"}, + {".vdp", "text/plain"}, + {".vdproj", "text/plain"}, + {".vdx", "application/vnd.ms-visio.viewer"}, + {".vml", "text/xml"}, + {".vscontent", "application/xml"}, + {".vsct", "text/xml"}, + {".vsd", "application/vnd.visio"}, + {".vsi", "application/ms-vsi"}, + {".vsix", "application/vsix"}, + {".vsixlangpack", "text/xml"}, + {".vsixmanifest", "text/xml"}, + {".vsmdi", "application/xml"}, + {".vspscc", "text/plain"}, + {".vss", "application/vnd.visio"}, + {".vsscc", "text/plain"}, + {".vssettings", "text/xml"}, + {".vssscc", "text/plain"}, + {".vst", "application/vnd.visio"}, + {".vstemplate", "text/xml"}, + {".vsto", "application/x-ms-vsto"}, + {".vsw", "application/vnd.visio"}, + {".vsx", "application/vnd.visio"}, + {".vtt", "text/vtt"}, + {".vtx", "application/vnd.visio"}, + {".wasm", "application/wasm"}, + {".wav", "audio/wav"}, + {".wave", "audio/wav"}, + {".wax", "audio/x-ms-wax"}, + {".wbk", "application/msword"}, + {".wbmp", "image/vnd.wap.wbmp"}, + {".wcm", "application/vnd.ms-works"}, + {".wdb", "application/vnd.ms-works"}, + {".wdp", "image/vnd.ms-photo"}, + {".webarchive", "application/x-safari-webarchive"}, + {".webm", "video/webm"}, + {".webp", "image/webp"}, /* https://en.wikipedia.org/wiki/WebP */ + {".webtest", "application/xml"}, + {".wiq", "application/xml"}, + {".wiz", "application/msword"}, + {".wks", "application/vnd.ms-works"}, + {".WLMP", "application/wlmoviemaker"}, + {".wlpginstall", "application/x-wlpg-detect"}, + {".wlpginstall3", "application/x-wlpg3-detect"}, + {".wm", "video/x-ms-wm"}, + {".wma", "audio/x-ms-wma"}, + {".wmd", "application/x-ms-wmd"}, + {".wmf", "application/x-msmetafile"}, + {".wml", "text/vnd.wap.wml"}, + {".wmlc", "application/vnd.wap.wmlc"}, + {".wmls", "text/vnd.wap.wmlscript"}, + {".wmlsc", "application/vnd.wap.wmlscriptc"}, + {".wmp", "video/x-ms-wmp"}, + {".wmv", "video/x-ms-wmv"}, + {".wmx", "video/x-ms-wmx"}, + {".wmz", "application/x-ms-wmz"}, + {".woff", "application/font-woff"}, + {".woff2", "application/font-woff2"}, + {".wpl", "application/vnd.ms-wpl"}, + {".wps", "application/vnd.ms-works"}, + {".wri", "application/x-mswrite"}, + {".wrl", "x-world/x-vrml"}, + {".wrz", "x-world/x-vrml"}, + {".wsc", "text/scriptlet"}, + {".wsdl", "text/xml"}, + {".wvx", "video/x-ms-wvx"}, + {".x", "application/directx"}, + {".xaf", "x-world/x-vrml"}, + {".xaml", "application/xaml+xml"}, + {".xap", "application/x-silverlight-app"}, + {".xbap", "application/x-ms-xbap"}, + {".xbm", "image/x-xbitmap"}, + {".xdr", "text/plain"}, + {".xht", "application/xhtml+xml"}, + {".xhtml", "application/xhtml+xml"}, + {".xla", "application/vnd.ms-excel"}, + {".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"}, + {".xlc", "application/vnd.ms-excel"}, + {".xld", "application/vnd.ms-excel"}, + {".xlk", "application/vnd.ms-excel"}, + {".xll", "application/vnd.ms-excel"}, + {".xlm", "application/vnd.ms-excel"}, + {".xls", "application/vnd.ms-excel"}, + {".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"}, + {".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"}, + {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {".xlt", "application/vnd.ms-excel"}, + {".xltm", "application/vnd.ms-excel.template.macroEnabled.12"}, + {".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, + {".xlw", "application/vnd.ms-excel"}, + {".xml", "text/xml"}, + {".xmp", "application/octet-stream"}, + {".xmta", "application/xml"}, + {".xof", "x-world/x-vrml"}, + {".XOML", "text/plain"}, + {".xpm", "image/x-xpixmap"}, + {".xps", "application/vnd.ms-xpsdocument"}, + {".xrm-ms", "text/xml"}, + {".xsc", "application/xml"}, + {".xsd", "text/xml"}, + {".xsf", "text/xml"}, + {".xsl", "text/xml"}, + {".xslt", "text/xml"}, + {".xsn", "application/octet-stream"}, + {".xss", "application/xml"}, + {".xspf", "application/xspf+xml"}, + {".xtp", "application/octet-stream"}, + {".xwd", "image/x-xwindowdump"}, + {".z", "application/x-compress"}, + {".zip", "application/zip"}, + + {"application/fsharp-script", ".fsx"}, + {"application/msaccess", ".adp"}, + {"application/msword", ".doc"}, + {"application/octet-stream", ".bin"}, + {"application/onenote", ".one"}, + {"application/postscript", ".eps"}, + {"application/step", ".step"}, + {"application/vnd.ms-excel", ".xls"}, + {"application/vnd.ms-powerpoint", ".ppt"}, + {"application/vnd.ms-works", ".wks"}, + {"application/vnd.visio", ".vsd"}, + {"application/x-director", ".dir"}, + {"application/x-msdos-program", ".exe"}, + {"application/x-shockwave-flash", ".swf"}, + {"application/x-x509-ca-cert", ".cer"}, + {"application/x-zip-compressed", ".zip"}, + {"application/xhtml+xml", ".xhtml"}, + { + "application/xml", ".xml" + }, // anomaly, .xml -> text/xml, but application/xml -> many things, but all are xml, so safest is .xml + {"audio/aac", ".AAC"}, + {"audio/aiff", ".aiff"}, + {"audio/basic", ".snd"}, + {"audio/mid", ".midi"}, + {"audio/mp4", ".m4a"}, // one way mapping only, mime -> ext + {"audio/wav", ".wav"}, + {"audio/x-m4a", ".m4a"}, + {"audio/x-mpegurl", ".m3u"}, + {"audio/x-pn-realaudio", ".ra"}, + {"audio/x-smd", ".smd"}, + {"image/bmp", ".bmp"}, + {"image/jpeg", ".jpg"}, + {"image/pict", ".pic"}, + {"image/png", ".png"}, // Defined in [RFC-2045], [RFC-2048] + { + "image/x-png", ".png" + }, // See https://www.w3.org/TR/PNG/#A-Media-type :"It is recommended that implementations also recognize the media type "image/x-png"." + {"image/tiff", ".tiff"}, + {"image/x-macpaint", ".mac"}, + {"image/x-quicktime", ".qti"}, + {"message/rfc822", ".eml"}, + {"text/calendar", ".ics"}, + {"text/html", ".html"}, + {"text/plain", ".txt"}, + {"text/scriptlet", ".wsc"}, + {"text/xml", ".xml"}, + {"video/3gpp", ".3gp"}, + {"video/3gpp2", ".3gp2"}, + {"video/mp4", ".mp4"}, + {"video/mpeg", ".mpg"}, + {"video/quicktime", ".mov"}, + {"video/vnd.dlna.mpeg-tts", ".m2t"}, + {"video/x-dv", ".dv"}, + {"video/x-la-asf", ".lsf"}, + {"video/x-ms-asf", ".asf"}, + {"x-world/x-vrml", ".xof"}, + + #endregion + }; + + var cache = mappings.ToList(); // need ToList() to avoid modifying while still enumerating + + foreach (var mapping in cache) + { + if (!mappings.ContainsKey(mapping.Value)) + { + mappings.Add(mapping.Value, mapping.Key); + } + } + + return mappings; + } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/Internals/Validation.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/Internals/Validation.cs new file mode 100644 index 00000000..d1bf65bf --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/Internals/Validation.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Lab.LineBot.SDK.Internals +{ + public class Validation + { + public static bool TryValidate(object contact, out List errors) + { + var context = new ValidationContext(contact, null, null); + errors = new List(); + return Validator.TryValidateObject(contact, context, errors, true); + } + + public static void Validate(object instance) + { + var context = new ValidationContext(instance, null, null); + Validator.ValidateObject(instance, context, true); + } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/Lab.LineBot.SDK.csproj b/Line/Lab.LineNotify/Lab.LineBot.SDK/Lab.LineBot.SDK.csproj new file mode 100644 index 00000000..600f4841 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/Lab.LineBot.SDK.csproj @@ -0,0 +1,11 @@ + + + + net5.0 + + + + + + + diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/LineNotifyProvider.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/LineNotifyProvider.cs new file mode 100644 index 00000000..503010ff --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/LineNotifyProvider.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Lab.LineBot.SDK.Internals; +using Lab.LineBot.SDK.Models; + +namespace Lab.LineBot.SDK +{ + public class LineNotifyProvider : ILineNotifyProvider + { + private static readonly string OAuth2Endpoint = "https://notify-bot.line.me/"; + private static readonly string ApiEndpoint = "https://notify-api.line.me/"; + + private static readonly Lazy s_oauthSocketsHandlerLazy = + new(() => + new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(10), + PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5), + MaxConnectionsPerServer = 10 + }); + + private static readonly Lazy s_apiSocketsHandlerLazy = + new(() => + new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(10), + PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5), + MaxConnectionsPerServer = 10 + }); + + public bool IsThrowInternalError { get; set; } = false; + + public async Task NotifyAsync(NotifyWithImageRequest request, + CancellationToken cancelToken) + { + Validation.Validate(request); + var url = $"api/notify?message={request.Message}"; + using var formDataContent = new MultipartFormDataContent(); + + var imageName = Path.GetFileName(request.FilePath); + var mimeType = MimeTypeMapping.GetMimeType(imageName); + var imageContent = new ByteArrayContent(request.FileBytes); + imageContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); + + formDataContent.Add(imageContent, "imageFile", imageName); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, url) + { + Headers = {Authorization = new AuthenticationHeaderValue("Bearer", request.AccessToken)}, + Content = formDataContent + }; + + using var client = this.CreateApiClient(); + var response = await client.SendAsync(httpRequest, cancelToken); + + if (response.StatusCode != HttpStatusCode.OK) + { + if (this.IsThrowInternalError) + { + var error = await response.Content.ReadAsStringAsync(cancelToken); + throw new LineNotifyProviderException(error); + } + } + + return await response.Content.ReadAsAsync(cancelToken); + } + + public async Task NotifyAsync(NotifyWithStickerRequest request, + CancellationToken cancelToken) + { + Validation.Validate(request); + + var url = "api/notify"; + var httpRequest = new HttpRequestMessage(HttpMethod.Post, url) + { + Headers = {Authorization = new AuthenticationHeaderValue("Bearer", request.AccessToken)}, + Content = new FormUrlEncodedContent(new Dictionary + { + {"message", request.Message}, + {"stickerPackageId", request.StickerPackageId}, + {"stickerId", request.StickerId}, + }), + }; + + using var client = this.CreateApiClient(); + var response = await client.SendAsync(httpRequest, cancelToken); + + if (response.StatusCode != HttpStatusCode.OK) + { + var error = await response.Content.ReadAsStringAsync(cancelToken); + if (this.IsThrowInternalError) + { + throw new LineNotifyProviderException(error); + } + + return new GenericResponse + { + Message = error, + }; + } + + return await response.Content.ReadAsAsync(cancelToken); + } + + public async Task GetAccessTokenInfoAsync(string accessToken, + CancellationToken cancelToken) + { + if (string.IsNullOrWhiteSpace(accessToken)) + { + throw new ArgumentNullException(nameof(accessToken)); + } + + var url = "api/status"; + var httpRequest = new HttpRequestMessage(HttpMethod.Get, url) + { + Headers = {Authorization = new AuthenticationHeaderValue("Bearer", accessToken)}, + Content = new FormUrlEncodedContent(new Dictionary()), + }; + + using var client = this.CreateApiClient(); + var response = await client.SendAsync(httpRequest, cancelToken); + + if (response.StatusCode != HttpStatusCode.OK) + { + if (this.IsThrowInternalError) + { + var error = await response.Content.ReadAsStringAsync(cancelToken); + throw new LineNotifyProviderException(error); + } + } + + var tokenInfo = await response.Content.ReadAsAsync(cancelToken); + tokenInfo.Limit = GetValue(response, "X-RateLimit-Limit"); + tokenInfo.ImageLimit = GetValue(response, "X-RateLimit-ImageLimit"); + tokenInfo.Remaining = GetValue(response, "X-RateLimit-Remaining"); + tokenInfo.ImageRemaining = GetValue(response, "X-RateLimit-ImageRemaining"); + tokenInfo.Reset = GetValue(response, "X-RateLimit-Reset"); + tokenInfo.ResetLocalTime = ToLocalTime(tokenInfo.Reset); + return tokenInfo; + } + + public string CreateAuthorizeCodeUrl(AuthorizeCodeUrlRequest request) + { + Validation.Validate(request); + + var url = "oauth/authorize"; + return $"{OAuth2Endpoint}" + + url + + "?response_type=code" + + "&scope=notify" + + "&response_mode=form_post" + + $"&client_id={request.ClientId}" + + $"&redirect_uri={request.CallbackUrl}" + + $"&state={request.State}" + ; + } + + public async Task GetAccessTokenAsync(TokenRequest request, + CancellationToken cancelToken) + { + Validation.Validate(request); + + var url = "oauth/token"; + + var content = new FormUrlEncodedContent(new Dictionary + { + {"grant_type", "authorization_code"}, + {"code", request.Code}, + {"redirect_uri", request.CallbackUrl}, + {"client_id", request.ClientId}, + {"client_secret", request.ClientSecret}, + }); + + using var client = this.CreateOAuth2Client(); + var response = await client.PostAsync(url, content, cancelToken); + string result = null; + + if (response.StatusCode != HttpStatusCode.OK) + { + if (this.IsThrowInternalError) + { + var error = await response.Content.ReadAsStringAsync(); + throw new LineNotifyProviderException(error); + } + } + + return await response.Content.ReadAsAsync(cancelToken); + } + + public async Task RevokeAsync(string accessToken, CancellationToken cancelToken) + { + if (string.IsNullOrWhiteSpace(accessToken)) + { + throw new ArgumentNullException(nameof(accessToken)); + } + + var url = "api/revoke"; + var httpRequest = new HttpRequestMessage(HttpMethod.Post, url) + { + Headers = {Authorization = new AuthenticationHeaderValue("Bearer", accessToken)}, + Content = new FormUrlEncodedContent(new Dictionary()), + }; + + using var client = this.CreateApiClient(); + var response = await client.SendAsync(httpRequest, cancelToken); + + if (response.StatusCode != HttpStatusCode.OK) + { + if (this.IsThrowInternalError) + { + var error = await response.Content.ReadAsStringAsync(); + throw new LineNotifyProviderException(error); + } + } + + return await response.Content.ReadAsAsync(cancelToken); + } + + private HttpClient CreateApiClient() + { + return new(s_apiSocketsHandlerLazy.Value) + { + BaseAddress = new Uri(ApiEndpoint) + }; + } + + private HttpClient CreateOAuth2Client() + { + return new(s_oauthSocketsHandlerLazy.Value) + { + BaseAddress = new Uri(OAuth2Endpoint) + }; + } + + private static T GetValue(HttpResponseMessage response, string key) + { + var result = default(T); + response.Headers.TryGetValues(key, out var values); + if (values == null) + { + return result; + } + + var content = values.FirstOrDefault(); + + return (T) Convert.ChangeType(content, typeof(T)); + } + + private static DateTime ToLocalTime(long source) + { + var timeOffset = DateTimeOffset.FromUnixTimeSeconds(source); + return timeOffset.DateTime.ToUniversalTime(); + } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/LineNotifyProviderException.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/LineNotifyProviderException.cs new file mode 100644 index 00000000..58a527dc --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/LineNotifyProviderException.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.Serialization; + +namespace Lab.LineBot.SDK +{ + [Serializable] + public class LineNotifyProviderException : Exception + { + // + // For guidelines regarding the creation of new exception types, see + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp + // and + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp + // + + public LineNotifyProviderException() + { + } + + public LineNotifyProviderException(string message) : base(message) + { + } + + public LineNotifyProviderException(string message, Exception inner) : base(message, inner) + { + } + + protected LineNotifyProviderException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/AuthorizeCodeUrlRequest.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/AuthorizeCodeUrlRequest.cs new file mode 100644 index 00000000..1d9300db --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/AuthorizeCodeUrlRequest.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace Lab.LineBot.SDK.Models +{ + public class AuthorizeCodeUrlRequest + { + [Required] + public string CallbackUrl { get; set; } + + [Required] + public string ClientId { get; set; } + + [Required] + public string State { get; set; } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/GenericResponse.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/GenericResponse.cs new file mode 100644 index 00000000..f39e70ab --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/GenericResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Lab.LineBot.SDK.Models +{ + public class GenericResponse + { + [JsonProperty("status")] + public int Status { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/NotifyWithImageRequest.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/NotifyWithImageRequest.cs new file mode 100644 index 00000000..1d55a823 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/NotifyWithImageRequest.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace Lab.LineBot.SDK.Models +{ + public class NotifyWithImageRequest + { + [Required] + public string Message { get; set; } + + [Required] + public string AccessToken { get; set; } + + public string FilePath { get; set; } + + public byte[] FileBytes { get; set; } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/NotifyWithStickerRequest.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/NotifyWithStickerRequest.cs new file mode 100644 index 00000000..b0da5d02 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/NotifyWithStickerRequest.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Lab.LineBot.SDK.Models +{ + public class NotifyWithStickerRequest + { + [Required] + public string Message { get; set; } + + [Required] + public string AccessToken { get; set; } + + //https://developers.line.biz/en/docs/messaging-api/sticker-list/#sticker-definitions + public string StickerPackageId { get; set; } + + public string StickerId { get; set; } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/TokenInfoResponse.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/TokenInfoResponse.cs new file mode 100644 index 00000000..8f6d9af9 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/TokenInfoResponse.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json; + +namespace Lab.LineBot.SDK.Models +{ + public class TokenInfoResponse : GenericResponse + { + [JsonProperty("targetType")] + public string TargetType { get; set; } + + [JsonProperty("target")] + public string Target { get; set; } + + public int Limit { get; set; } + + public int ImageLimit { get; set; } + + public int Remaining { get; set; } + + public int ImageRemaining { get; set; } + + public int Reset { get; set; } + + public DateTime ResetLocalTime { get; set; } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/TokenRequest.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/TokenRequest.cs new file mode 100644 index 00000000..07288524 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/TokenRequest.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace Lab.LineBot.SDK.Models +{ + public class TokenRequest + { + [Required] + public string Code { get; set; } + + [Required] + public string ClientId { get; set; } + + [Required] + public string ClientSecret { get; set; } + + [Required] + public string CallbackUrl { get; set; } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/TokenResponse.cs b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/TokenResponse.cs new file mode 100644 index 00000000..8d72d9dd --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineBot.SDK/Models/TokenResponse.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Lab.LineBot.SDK.Models +{ + public class TokenResponse : GenericResponse + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/1.jpg b/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/1.jpg new file mode 100644 index 00000000..8852efa4 Binary files /dev/null and b/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/1.jpg differ diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/Lab.LineNotify.Service.TestProject.csproj b/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/Lab.LineNotify.Service.TestProject.csproj new file mode 100644 index 00000000..4e97586f --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/Lab.LineNotify.Service.TestProject.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + + + Always + + + + diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/LineNotifyProviderTests.cs b/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/LineNotifyProviderTests.cs new file mode 100644 index 00000000..fd937ee7 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/LineNotifyProviderTests.cs @@ -0,0 +1,62 @@ +using System.IO; +using System.Threading; +using Lab.LineBot.SDK; +using Lab.LineBot.SDK.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.LineNotify.Service.TestProject +{ + [TestClass] + public class LineNotifyProviderTests + { + [TestMethod] + public void 取得AccessToken狀態() + { + var provider = new LineNotifyProvider(); + var response = provider + .GetAccessTokenInfoAsync("3lZwryen62tiQ4BKfh3uH3NFoFtALF4SrfgLWMIKrXh", + CancellationToken.None).Result; + Assert.AreEqual(200, response.Status); + } + + [TestMethod] + public void 發送訊息和表情() + { + var provider = new LineNotifyProvider(); + var response = provider.NotifyAsync(new NotifyWithStickerRequest + { + AccessToken = "3lZwryen62tiQ4BKfh3uH3NFoFtALF4SrfgLWMIKrXh", + Message = "HI~請給我黃金", + StickerPackageId = 1.ToString(), + StickerId = 113.ToString() + }, CancellationToken.None) + .Result; + Assert.AreEqual(200, response.Status); + } + + [TestMethod] + public void 發送訊息和圖片() + { + var provider = new LineNotifyProvider(); + var response = provider.NotifyAsync(new NotifyWithImageRequest + { + AccessToken = "3lZwryen62tiQ4BKfh3uH3NFoFtALF4SrfgLWMIKrXh", + Message = "HI~請給我黃金", + FilePath = "1.jpg", + FileBytes = File.ReadAllBytes("1.jpg") + }, CancellationToken.None) + .Result; + Assert.AreEqual(200, response.Status); + } + + [TestMethod] + public void 註銷AccessToken() + { + var provider = new LineNotifyProvider(); + var response = provider.RevokeAsync("3lZwryen62tiQ4BKfh3uH3NFoFtALF4SrfgLWMIKrXh", + CancellationToken.None) + .Result; + Assert.AreEqual(200, response.Status); + } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/Tests.cs b/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/Tests.cs new file mode 100644 index 00000000..8282edaa --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service.TestProject/Tests.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.LineNotify.Service.TestProject +{ + [TestClass] + public class Tests + { + private readonly Dictionary> _pool; + + public Tests() + { + this._pool = new Dictionary>(); + this._pool.Add("info", this.GetInfo); + this._pool.Add("status", this.GetStatus); + } + + [TestMethod] + public void GetInfo() + { + var key = "info"; + var response = this.Get(key, "yao", 18); + } + + [TestMethod] + public void GetStatus() + { + var key = "status"; + var response = this.Get(key, "192.168.1.1", 1024); + } + + private TResponse Get(string key, string p1, int? p2) + { + if (this._pool.ContainsKey(key) == false) + { + return default; + } + + var func = this._pool[key]; + return (TResponse) func.Invoke(p1, p2); + } + + private InfoResponse GetInfo(string p1, int? p2) + { + return new InfoResponse + { + Name = p1, + Age = p2 + }; + } + private InfoResponse GetInfo1(string p1, int? p2) + { + return new InfoResponse + { + Name = p1, + }; + } + private StatusResponse GetStatus(string p1, int? p2) + { + return new StatusResponse + { + Code = p2.Value, + IpAddress = p1 + }; + } + } + + internal class Content + { + public string TypeName { get; set; } + } + + internal class StatusResponse + { + public int Code { get; set; } + + public string IpAddress { get; set; } + } + + internal class StatusRequest + { + public int Code { get; set; } + } + + internal class InfoRequest + { + public string Name { get; set; } + } + + internal class InfoResponse + { + public string Name { get; set; } + + public int? Age { get; set; } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service/Controllers/AuthorizeController.cs b/Line/Lab.LineNotify/Lab.LineNotify.Service/Controllers/AuthorizeController.cs new file mode 100644 index 00000000..590b16e8 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service/Controllers/AuthorizeController.cs @@ -0,0 +1,61 @@ +using System.Threading; +using System.Threading.Tasks; +using Lab.LineBot.SDK; +using Lab.LineBot.SDK.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Lab.LineNotify.Service.Controllers +{ + [ApiController] + [Route("[controller]")] + public class AuthorizeCodeController : ControllerBase + { + private readonly IConfiguration _config; + private readonly ILineNotifyProvider _lineNotifyProvider; + private readonly ILogger _logger; + + public AuthorizeCodeController(ILogger logger, + IConfiguration config, + ILineNotifyProvider lineNotifyProvider) + { + this._logger = logger; + this._config = config; + this._lineNotifyProvider = lineNotifyProvider; + } + + [HttpPost] + public async Task Post([FromForm] IFormCollection data, CancellationToken cancelToken) + { + if (data.TryGetValue("code", out var code) == false) + { + this.ModelState.AddModelError("code 欄位", "必填"); + return this.BadRequest(this.ModelState); + } + + if (data.TryGetValue("state", out var state) == false) + { + this.ModelState.AddModelError("state 欄位", "必填"); + return this.BadRequest(this.ModelState); + } + + var config = this._config; + var lineNotifyProvider = this._lineNotifyProvider; + + var lineConfig = config.GetSection("LineNotify"); + var request = new TokenRequest + { + Code = code, + ClientId = lineConfig.GetValue("clientId"), + ClientSecret = lineConfig.GetValue("clientSecret"), + CallbackUrl = lineConfig.GetValue("redirectUri"), + }; + var accessToken = await lineNotifyProvider.GetAccessTokenAsync(request, cancelToken); + + //TODO:應該記錄在你的 DB 或是其它地方,不應該回傳 Access Token + return this.Ok(accessToken); + } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service/Lab.LineNotify.Service.csproj b/Line/Lab.LineNotify/Lab.LineNotify.Service/Lab.LineNotify.Service.csproj new file mode 100644 index 00000000..7cd965d3 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service/Lab.LineNotify.Service.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + + + + + + + + + + + + + diff --git a/Host/ConsoleAppNet48/Program.cs b/Line/Lab.LineNotify/Lab.LineNotify.Service/Program.cs similarity index 50% rename from Host/ConsoleAppNet48/Program.cs rename to Line/Lab.LineNotify/Lab.LineNotify.Service/Program.cs index ee76ac29..af088d45 100644 --- a/Host/ConsoleAppNet48/Program.cs +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service/Program.cs @@ -1,17 +1,14 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace ConsoleAppNet48 +namespace Lab.LineNotify.Service { public class Program { public static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddHostedService(); - }); + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); } public static void Main(string[] args) diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service/Properties/launchSettings.json b/Line/Lab.LineNotify/Lab.LineNotify.Service/Properties/launchSettings.json new file mode 100644 index 00000000..39776679 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:24864", + "sslPort": 44336 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Lab.LineNotify.Service": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service/ServiceModels/ReviceAuthorizeCodeRequest.cs b/Line/Lab.LineNotify/Lab.LineNotify.Service/ServiceModels/ReviceAuthorizeCodeRequest.cs new file mode 100644 index 00000000..ff163342 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service/ServiceModels/ReviceAuthorizeCodeRequest.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace Lab.LineNotify.Service.ServiceModels +{ + public class ReceiveAuthorizeCodeRequest + { + + [JsonProperty("code")] + public string Code { get; set; } + + [JsonProperty("state")] + public int State { get; set; } + + [JsonProperty("error")] + public string Error { get; set; } + + [JsonProperty("error_description")] + public string ErrorDescription { get; set; } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service/Startup.cs b/Line/Lab.LineNotify/Lab.LineNotify.Service/Startup.cs new file mode 100644 index 00000000..1767dc7b --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service/Startup.cs @@ -0,0 +1,52 @@ +using Lab.LineBot.SDK; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; + +namespace Lab.LineNotify.Service +{ + public class Startup + { + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + { + this.Configuration = configuration; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Lab.LineNotify.Service v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers().AddNewtonsoftJson(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", + new OpenApiInfo {Title = "Lab.LineNotify.Service", Version = "v1"}); + }); + services.AddSingleton(); + services.AddSingleton(p => p.GetService()); + } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service/WeatherForecast.cs b/Line/Lab.LineNotify/Lab.LineNotify.Service/WeatherForecast.cs new file mode 100644 index 00000000..1a6b47f7 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service/WeatherForecast.cs @@ -0,0 +1,15 @@ +using System; + +namespace Lab.LineNotify.Service +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int) (this.TemperatureC / 0.5556); + + public string Summary { get; set; } + } +} \ No newline at end of file diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service/appsettings.Development.json b/Line/Lab.LineNotify/Lab.LineNotify.Service/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Line/Lab.LineNotify/Lab.LineNotify.Service/appsettings.json b/Line/Lab.LineNotify/Lab.LineNotify.Service/appsettings.json new file mode 100644 index 00000000..5f313c19 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.Service/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "LineNotify": { + "clientId": "Ppu33o7F0c2BTcryJ3PVDQ", + "clientSecret": "cf9ya2A9HA1TzWeXr7GF0ixqCC6vYtIb0Yq8KkOMSwj", + "redirectUri": "https://localhost:5001/AuthorizeCode", + "state": "NO_STATE" + } +} diff --git a/Line/Lab.LineNotify/Lab.LineNotify.sln b/Line/Lab.LineNotify/Lab.LineNotify.sln new file mode 100644 index 00000000..461679d2 --- /dev/null +++ b/Line/Lab.LineNotify/Lab.LineNotify.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.LineNotify.Service", "Lab.LineNotify.Service\Lab.LineNotify.Service.csproj", "{AFD89464-B981-4C9D-8336-5E2A9A8A0F60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.LineBot.SDK", "Lab.LineBot.SDK\Lab.LineBot.SDK.csproj", "{0EA7AE1F-ECF9-4BC8-BA95-CA2F8FA1E95F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.LineNotify.Service.TestProject", "Lab.LineNotify.Service.TestProject\Lab.LineNotify.Service.TestProject.csproj", "{8355BA66-8285-407B-B8D4-3208E66B2D6B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AFD89464-B981-4C9D-8336-5E2A9A8A0F60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFD89464-B981-4C9D-8336-5E2A9A8A0F60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFD89464-B981-4C9D-8336-5E2A9A8A0F60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFD89464-B981-4C9D-8336-5E2A9A8A0F60}.Release|Any CPU.Build.0 = Release|Any CPU + {0EA7AE1F-ECF9-4BC8-BA95-CA2F8FA1E95F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EA7AE1F-ECF9-4BC8-BA95-CA2F8FA1E95F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EA7AE1F-ECF9-4BC8-BA95-CA2F8FA1E95F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EA7AE1F-ECF9-4BC8-BA95-CA2F8FA1E95F}.Release|Any CPU.Build.0 = Release|Any CPU + {8355BA66-8285-407B-B8D4-3208E66B2D6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8355BA66-8285-407B-B8D4-3208E66B2D6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8355BA66-8285-407B-B8D4-3208E66B2D6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8355BA66-8285-407B-B8D4-3208E66B2D6B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation.UnitTest/Lab.DictionaryFluentValidation.UnitTest.csproj b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation.UnitTest/Lab.DictionaryFluentValidation.UnitTest.csproj new file mode 100644 index 00000000..3487bd02 --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation.UnitTest/Lab.DictionaryFluentValidation.UnitTest.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation.UnitTest/ProfileValidatorTests.cs b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation.UnitTest/ProfileValidatorTests.cs new file mode 100644 index 00000000..af85a89c --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation.UnitTest/ProfileValidatorTests.cs @@ -0,0 +1,204 @@ +using System.Collections.Generic; +using System.Linq; +using Lab.DictionaryFluentValidation.Validators; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.DictionaryFluentValidation.UnitTest; + +[TestClass] +public class ProfileValidatorTests +{ + [TestMethod] + public void aaaa() + { + var profileValidator = new ProfileTypeValidator(); + var data = new Dictionary + { + { "name", new { firstName = "yao", lastName = "yu", fullName = "yao-chang.yu" } }, + { "birthday", new { year = 2000, month = 2, day = 28 } }, + { "contactEmail", "yao@aa.bb" }, + }; + var validationResult = profileValidator.Validate(data); + + data = new Dictionary + { + { "gender", "公的" }, + }; + validationResult = profileValidator.Validate(data); + + data = new Dictionary + { + { "Name", null }, + }; + + validationResult = profileValidator.Validate(data); + data = new Dictionary + { + { "name", new { firstName = "yao", lastName = "", fullName = "" } }, + }; + validationResult = profileValidator.Validate(data); + data = new Dictionary + { + { "Hi", null }, + }; + validationResult = profileValidator.Validate(data); + } + + [TestMethod] + public void Key區分大小寫() + { + var data = new Dictionary + { + { "Name", null }, + }; + + var profileValidator = new ProfileTypeValidator(); + var validationResult = profileValidator.Validate(data); + Assert.AreEqual(false, validationResult.IsValid); + var actualError = validationResult.Errors.First(); + Assert.AreEqual("Name", actualError.PropertyName); + Assert.AreEqual("NotSupportValidator", actualError.ErrorCode); + Assert.AreEqual("'Name' column not support", actualError.ErrorMessage); + } + + [TestMethod] + public void 二月三十是非法日期() + { + var data = new Dictionary + { + { "birthday", new { year = 2000, month = 2, day = 30 } }, + }; + + var profileValidator = new ProfileTypeValidator(); + var validationResult = profileValidator.Validate(data); + Assert.AreEqual(false, validationResult.IsValid); + var actualError = validationResult.Errors.First(); + Assert.AreEqual("birthday", actualError.PropertyName); + Assert.AreEqual(nameof(BirthdayTypeValidator), actualError.ErrorCode); + Assert.AreEqual("year:2000,month:2,day:30 is invalid date format", actualError.ErrorMessage); + } + + [TestMethod] + public void 日期內容為非法值() + { + var data = new Dictionary + { + { "birthday", null }, + }; + + var profileValidator = new ProfileTypeValidator(); + var validationResult = profileValidator.Validate(data); + Assert.AreEqual(true, validationResult.IsValid); + } + + [TestMethod] + public void 必填欄位為空() + { + var data = new Dictionary + { + { "name", new { firstName = "yao", lastName = "", fullName = "" } }, + }; + var profileValidator = new ProfileTypeValidator(); + var validationResult = profileValidator.Validate(data); + + // Assert.AreEqual(false, validationResult.IsValid); + var actualError = validationResult.Errors.First(); + Assert.AreEqual("name.fullName", actualError.PropertyName); + Assert.AreEqual("NotEmptyValidator", actualError.ErrorCode); + Assert.AreEqual("'name.fullName' must not be empty.", actualError.ErrorMessage); + } + + [TestMethod] + public void 沒有年是非法日期() + { + var data = new Dictionary + { + { "birthday", new { month = 2, day = 30 } }, + }; + + var profileValidator = new ProfileTypeValidator(); + var validationResult = profileValidator.Validate(data); + Assert.AreEqual(false, validationResult.IsValid); + var actualError = validationResult.Errors.First(); + Assert.AreEqual("birthday.year", actualError.PropertyName); + Assert.AreEqual(nameof(BirthdayTypeValidator), actualError.ErrorCode); + Assert.AreEqual("'birthday.year' must not be empty.", actualError.ErrorMessage); + } + + [TestMethod] + public void 使用不支援的Key() + { + var data = new Dictionary + { + { "Hi", null }, + }; + + var profileValidator = new ProfileTypeValidator(); + var validationResult = profileValidator.Validate(data); + Assert.AreEqual(false, validationResult.IsValid); + var actualError = validationResult.Errors.First(); + Assert.AreEqual("Hi", actualError.PropertyName); + Assert.AreEqual("NotSupportValidator", actualError.ErrorCode); + Assert.AreEqual("'Hi' column not support", actualError.ErrorMessage); + } + + [TestMethod] + public void 使用支援的Key() + { + var data = new Dictionary + { + { "name", null }, + }; + + var profileValidator = new ProfileTypeValidator(); + var validationResult = profileValidator.Validate(data); + Assert.AreEqual(true, validationResult.IsValid); + } + + [TestMethod] + public void 性別格式錯誤() + { + var data = new Dictionary + { + { "gender", "公的" }, + }; + var profileValidator = new ProfileTypeValidator(); + var validationResult = profileValidator.Validate(data); + Assert.AreEqual(false, validationResult.IsValid); + var actualError = validationResult.Errors.First(); + Assert.AreEqual("gender", actualError.PropertyName); + Assert.AreEqual(nameof(GenderTypeValidator), actualError.ErrorCode); + Assert.AreEqual("'公的' is invalid value.", actualError.ErrorMessage); + } + + [TestMethod] + public void 通過驗證() + { + var data = new Dictionary + { + { "name", new { firstName = "yao", lastName = "yu", fullName = "yao-chang.yu" } }, + { "birthday", new { year = 2000, month = 2, day = 28 } }, + { "contactEmail", "yao@aa.bb" }, + }; + + var profileValidator = new ProfileTypeValidator(); + var validationResult = profileValidator.Validate(data); + Assert.AreEqual(true, validationResult.IsValid); + } + + [TestMethod] + public void 郵件格式錯誤() + { + var data = new Dictionary + { + { "contactEmail", "yao" }, + }; + var profileValidator = new ProfileTypeValidator(); + var validationResult = profileValidator.Validate(data); + Assert.AreEqual(false, validationResult.IsValid); + var actualError = validationResult.Errors.First(); + Assert.AreEqual("contactEmail", actualError.PropertyName); + Assert.AreEqual(nameof(EmailTypeValidator), actualError.ErrorCode); + Assert.AreEqual("'contactEmail' is not a valid email address.", actualError.ErrorMessage); + } +} \ No newline at end of file diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Assistants/ProfileAssistants.cs b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Assistants/ProfileAssistants.cs new file mode 100644 index 00000000..f2ce8fb0 --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Assistants/ProfileAssistants.cs @@ -0,0 +1,23 @@ +using System.Reflection; + +namespace Lab.DictionaryFluentValidation.Assistants; + +public class ProfileAssistants +{ + public static Dictionary GetFieldNames() + { + var type = typeof(T); + + var bindingFlags = BindingFlags.Public + | BindingFlags.Static + ; + var results = new Dictionary(); + var fieldInfosInfos = type.GetFields(bindingFlags); + foreach (var fieldInfo in fieldInfosInfos) + { + results.Add(fieldInfo.GetValue(null).ToString(), null); + } + + return results; + } +} \ No newline at end of file diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/BirthdayTypeNames.cs b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/BirthdayTypeNames.cs new file mode 100644 index 00000000..0a5a89ba --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/BirthdayTypeNames.cs @@ -0,0 +1,20 @@ +using Lab.DictionaryFluentValidation.Assistants; + +namespace Lab.DictionaryFluentValidation.Fields; + +public class BirthdayTypeNames +{ + public const string Year = "year"; + public const string Month = "month"; + public const string Day = "day"; + + private static readonly Lazy> s_fieldNamesLazy = + new(() => ProfileAssistants.GetFieldNames()); + + private static Dictionary FieldNames => s_fieldNamesLazy.Value; + + public static Dictionary GetFieldNames() + { + return FieldNames; + } +} \ No newline at end of file diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/GenderTypeValues.cs b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/GenderTypeValues.cs new file mode 100644 index 00000000..670504e1 --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/GenderTypeValues.cs @@ -0,0 +1,20 @@ +using Lab.DictionaryFluentValidation.Assistants; + +namespace Lab.DictionaryFluentValidation.Fields; + +public class GenderTypeValues +{ + public const string Male = "male"; + public const string Female = "female"; + public const string NotAvailable = "notAvailable"; + + private static readonly Lazy> s_fieldValuesLazy = + new(() => ProfileAssistants.GetFieldNames()); + + private static Dictionary FieldValues => s_fieldValuesLazy.Value; + + public static Dictionary GetFieldValues() + { + return FieldValues; + } +} \ No newline at end of file diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/NameTypeNames.cs b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/NameTypeNames.cs new file mode 100644 index 00000000..0034bbe6 --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/NameTypeNames.cs @@ -0,0 +1,20 @@ +using Lab.DictionaryFluentValidation.Assistants; + +namespace Lab.DictionaryFluentValidation.Fields; + +public class NameTypeNames +{ + public const string FirstName = "firstName"; + public const string LastName = "lastName"; + public const string FullName = "fullName"; + + private static readonly Lazy> s_fieldNamesLazy = + new(() => ProfileAssistants.GetFieldNames()); + + private static Dictionary FieldNames => s_fieldNamesLazy.Value; + + public static Dictionary GetFieldNames() + { + return FieldNames; + } +} \ No newline at end of file diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/ProfileTypeNames.cs b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/ProfileTypeNames.cs new file mode 100644 index 00000000..e00d1b53 --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Fields/ProfileTypeNames.cs @@ -0,0 +1,21 @@ +using Lab.DictionaryFluentValidation.Assistants; + +namespace Lab.DictionaryFluentValidation.Fields; + +public class ProfileTypeNames +{ + public const string Name = "name"; + public const string Gender = "gender"; + public const string Birthday = "birthday"; + public const string ContactEmail = "contactEmail"; + + private static readonly Lazy> s_fieldNamesLazy = + new(() => ProfileAssistants.GetFieldNames()); + + private static Dictionary FieldNames => s_fieldNamesLazy.Value; + + public static Dictionary GetFieldNames() + { + return FieldNames; + } +} \ No newline at end of file diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Lab.DictionaryFluentValidation.csproj b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Lab.DictionaryFluentValidation.csproj new file mode 100644 index 00000000..f03eb636 --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Lab.DictionaryFluentValidation.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/BirthdayTypeValidator.cs b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/BirthdayTypeValidator.cs new file mode 100644 index 00000000..e88c1704 --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/BirthdayTypeValidator.cs @@ -0,0 +1,97 @@ +using FluentValidation; +using FluentValidation.Results; +using Lab.DictionaryFluentValidation.Fields; + +namespace Lab.DictionaryFluentValidation.Validators; + +public class BirthdayTypeValidator : AbstractValidator +{ + private const string ErrorCode = nameof(BirthdayTypeValidator); + private readonly string _propertyName; + + public BirthdayTypeValidator(string propertyName) + { + this._propertyName = propertyName; + } + + private bool HasRequireField(ValidationContext context, Dictionary srcBirthdayFields, + Dictionary destBirthdayFields) + { + var isValid = true; + foreach (var srcField in srcBirthdayFields) + { + var srcKey = srcField.Key; + if (destBirthdayFields.ContainsKey(srcKey) == false) + { + var propertyName = $"{_propertyName}.{srcKey}"; + var validationFailure = new ValidationFailure(propertyName, $"'{propertyName}' must not be empty.") + { + ErrorCode = ErrorCode + }; + context.AddFailure(validationFailure); + isValid = false; + } + } + + return isValid; + } + + /// + /// return true 繼續往下驗證 + /// https://docs.fluentvalidation.net/en/latest/advanced.html?highlight=PreValidate#prevalidate + /// + /// + /// + /// + protected override bool PreValidate(ValidationContext context, ValidationResult result) + { + var isValid = true; + var instance = context.InstanceToValidate; + if (instance == null) + { + return isValid; + } + + var propertyInfos = instance.GetType().GetProperties(); + var birthday = new Dictionary(); + foreach (var propertyInfo in propertyInfos) + { + var value = propertyInfo.GetValue(instance); + if (value == null) + { + continue; + } + + birthday.Add(propertyInfo.Name, Convert.ToInt32(value)); + } + + var srcBirthdayFields = BirthdayTypeNames.GetFieldNames(); + isValid = HasRequireField(context, srcBirthdayFields, birthday); + if (isValid == false) + { + return isValid; + } + + var year = birthday[BirthdayTypeNames.Year]; + var month = birthday[BirthdayTypeNames.Month]; + var day = birthday[BirthdayTypeNames.Day]; + try + { + var birthday2 = new DateTime(year, month, day); + } + catch (Exception e) + { + var errorMsg = $"{BirthdayTypeNames.Year}:{year}," + + $"{BirthdayTypeNames.Month}:{month}," + + $"{BirthdayTypeNames.Day}:{day} is invalid date format"; + + var validationFailure = new ValidationFailure(this._propertyName, errorMsg) + { + ErrorCode = ErrorCode + }; + context.AddFailure(validationFailure); + } + + return isValid; + } +} \ No newline at end of file diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/EmailTypeValidator.cs b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/EmailTypeValidator.cs new file mode 100644 index 00000000..9d3bcae3 --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/EmailTypeValidator.cs @@ -0,0 +1,38 @@ +using FluentValidation; +using FluentValidation.Results; + +namespace Lab.DictionaryFluentValidation.Validators; + +public class EmailTypeValidator : AbstractValidator +{ + private readonly string _propertyName; + + public EmailTypeValidator(string propertyName) + { + this._propertyName = propertyName; + } + + /// + /// return true 繼續往下驗證 + /// https://docs.fluentvalidation.net/en/latest/advanced.html?highlight=PreValidate#prevalidate + /// + /// + /// + /// + protected override bool PreValidate(ValidationContext context, ValidationResult result) + { + var isValid = true; + var instance = context.InstanceToValidate; + if (instance == null) + { + return isValid; + } + + this.RuleFor(p => p.ToString()) + .EmailAddress() + .WithName(this._propertyName) + .WithErrorCode(nameof(EmailTypeValidator)) + ; + return isValid; + } +} \ No newline at end of file diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/GenderTypeValidator.cs b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/GenderTypeValidator.cs new file mode 100644 index 00000000..998dbfab --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/GenderTypeValidator.cs @@ -0,0 +1,47 @@ +using FluentValidation; +using FluentValidation.Results; +using Lab.DictionaryFluentValidation.Fields; + +namespace Lab.DictionaryFluentValidation.Validators; + +public class GenderTypeValidator : AbstractValidator +{ + private const string ErrorCode = nameof(GenderTypeValidator); + private readonly string _propertyName; + + public GenderTypeValidator(string propertyName) + { + this._propertyName = propertyName; + } + + /// + /// return true 繼續往下驗證 + /// https://docs.fluentvalidation.net/en/latest/advanced.html?highlight=PreValidate#prevalidate + /// + /// + /// + /// + protected override bool PreValidate(ValidationContext context, ValidationResult result) + { + var isValid = true; + var instance = context.InstanceToValidate; + if (instance == null) + { + return isValid; + } + + var srcValues = GenderTypeValues.GetFieldValues(); + var destValue = instance.ToString(); + if (srcValues.ContainsKey(destValue) == false) + { + var validationFailure = new ValidationFailure(this._propertyName, + $"'{destValue}' is invalid value.") + { + ErrorCode = ErrorCode + }; + context.AddFailure(validationFailure); + } + + return isValid; + } +} \ No newline at end of file diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/NameTypeValidator.cs b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/NameTypeValidator.cs new file mode 100644 index 00000000..bd3914d3 --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/NameTypeValidator.cs @@ -0,0 +1,61 @@ +using FluentValidation; +using FluentValidation.Results; +using Lab.DictionaryFluentValidation.Fields; + +namespace Lab.DictionaryFluentValidation.Validators; + +public class NameTypeValidator : AbstractValidator +{ + private readonly string _propertyName; + + public NameTypeValidator(string propertyName) + { + this._propertyName = propertyName; + } + + /// + /// return true 繼續往下驗證 + /// https://docs.fluentvalidation.net/en/latest/advanced.html?highlight=PreValidate#prevalidate + /// + /// + /// + /// + protected override bool PreValidate(ValidationContext context, ValidationResult result) + { + var isValid = true; + var instance = context.InstanceToValidate; + if (instance == null) + { + return isValid; + } + + var propertyInfos = instance.GetType().GetProperties(); + foreach (var propertyInfo in propertyInfos) + { + var value = propertyInfo.GetValue(instance); + if (value == null) + { + continue; + } + + var propertyName = $"{this._propertyName}.{propertyInfo.Name}"; + switch (propertyInfo.Name) + { + case NameTypeNames.FirstName: + break; + case NameTypeNames.LastName: + break; + case NameTypeNames.FullName: + this.RuleFor(p => value) + .NotEmpty() + .WithName(propertyName) + .OverridePropertyName(propertyName) + ; + + break; + } + } + + return isValid; + } +} \ No newline at end of file diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/ProfileTypeValidator.cs b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/ProfileTypeValidator.cs new file mode 100644 index 00000000..7336f78c --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryFluentValidation/Validators/ProfileTypeValidator.cs @@ -0,0 +1,151 @@ +using FluentValidation; +using FluentValidation.Results; +using Lab.DictionaryFluentValidation.Fields; + +namespace Lab.DictionaryFluentValidation.Validators; + +public class ProfileTypeValidator : AbstractValidator> +{ + private static readonly Lazy s_emailTypeValidatorLazy = + new(() => new EmailTypeValidator(ProfileTypeNames.ContactEmail)); + + private static readonly Lazy s_nameTypeValidator = + new Lazy(() => new NameTypeValidator(ProfileTypeNames.Name)); + + private static readonly Lazy s_birthdayTypeValidatorLazy = + new(() => new BirthdayTypeValidator(ProfileTypeNames.Birthday)); + + private static readonly Lazy s_genderTypeValidatorLazy = + new(() => new GenderTypeValidator(ProfileTypeNames.Gender)); + + private static EmailTypeValidator EmailTypeValidator => s_emailTypeValidatorLazy.Value; + + private static NameTypeValidator NameTypeValidator => s_nameTypeValidator.Value; + + private static BirthdayTypeValidator BirthdayTypeValidator => s_birthdayTypeValidatorLazy.Value; + + private static GenderTypeValidator GenderTypeValidator => s_genderTypeValidatorLazy.Value; + + private static bool IsNotSupportFields(ValidationContext> context) + { + var instances = context.InstanceToValidate; + var isNotSupports = new List(); + foreach (var item in instances) + { + var fieldName = item.Key; + var fieldValue = item.Value; + + switch (fieldName) + { + case ProfileTypeNames.Name: + isNotSupports.Add(IsNotSupportNestFields(NameTypeNames.GetFieldNames(), fieldValue, context)); + break; + case ProfileTypeNames.Birthday: + isNotSupports.Add(IsNotSupportNestFields(BirthdayTypeNames.GetFieldNames(), fieldValue, context)); + break; + default: + isNotSupports.Add(IsNotSupportFields(ProfileTypeNames.GetFieldNames(), fieldName, context)); + break; + } + } + + return isNotSupports.Any(p => p); + } + + private static bool IsNotSupportFields(Dictionary sourceFields, + string destFieldName, + ValidationContext> context) + { + var isNotSupport = sourceFields.ContainsKey(destFieldName) == false; + if (isNotSupport) + { + var failure = new ValidationFailure(destFieldName, + $"'{destFieldName}' column not support") + { + ErrorCode = "NotSupportValidator", + }; + context.AddFailure(failure); + } + + return isNotSupport; + } + + private static bool IsNotSupportNestFields(Dictionary sourceFields, + object destValue, + ValidationContext> context) + { + if (destValue == null) + { + return false; + } + + var isNotSupports = new List(); + + var propertyInfos = destValue.GetType().GetProperties(); + foreach (var propertyInfo in propertyInfos) + { + isNotSupports.Add(IsNotSupportFields(sourceFields, propertyInfo.Name, context)); + } + + return isNotSupports.Any(p => p); + } + + protected override bool PreValidate(ValidationContext> context, ValidationResult result) + { + if (IsNotSupportFields(context)) + { + return false; + } + + var instances = context.InstanceToValidate; + this.SetValidateRule(instances); + + return true; + } + + private void SetValidateRule(Dictionary instances) + { + foreach (var item in instances) + { + var fieldName = item.Key; + var fieldValue = item.Value; + if (fieldValue == null) + { + continue; + } + + switch (fieldName) + { + case ProfileTypeNames.ContactEmail: + { + this.RuleFor(p => p[fieldName]) + .SetValidator(p => EmailTypeValidator) + ; + + break; + } + case ProfileTypeNames.Name: + { + this.RuleFor(p => p[fieldName]) + .SetValidator(p => NameTypeValidator) + ; + break; + } + case ProfileTypeNames.Birthday: + { + this.RuleFor(p => p[fieldName]) + .SetValidator(p => BirthdayTypeValidator) + ; + break; + } + case ProfileTypeNames.Gender: + { + this.RuleFor(p => p[fieldName]) + .SetValidator(p => GenderTypeValidator) + ; + break; + } + } + } + } +} \ No newline at end of file diff --git a/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryValidation.sln b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryValidation.sln new file mode 100644 index 00000000..ab18b294 --- /dev/null +++ b/ModelValidation/Lab.DictionaryValidation/Lab.DictionaryValidation.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.DictionaryFluentValidation", "Lab.DictionaryFluentValidation\Lab.DictionaryFluentValidation.csproj", "{F351E4A0-8F8E-4CEF-BE2A-D158E420DFB6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.DictionaryFluentValidation.UnitTest", "Lab.DictionaryFluentValidation.UnitTest\Lab.DictionaryFluentValidation.UnitTest.csproj", "{3C5718DF-EA0D-472B-8067-B5C736923125}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F351E4A0-8F8E-4CEF-BE2A-D158E420DFB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F351E4A0-8F8E-4CEF-BE2A-D158E420DFB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F351E4A0-8F8E-4CEF-BE2A-D158E420DFB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F351E4A0-8F8E-4CEF-BE2A-D158E420DFB6}.Release|Any CPU.Build.0 = Release|Any CPU + {3C5718DF-EA0D-472B-8067-B5C736923125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C5718DF-EA0D-472B-8067-B5C736923125}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C5718DF-EA0D-472B-8067-B5C736923125}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C5718DF-EA0D-472B-8067-B5C736923125}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/Lab.EFCoreBulk.UnitTest.csproj b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/Lab.EFCoreBulk.UnitTest.csproj new file mode 100644 index 00000000..07ee4013 --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/Lab.EFCoreBulk.UnitTest.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + + + + + + diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/MsTestHook.cs b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/MsTestHook.cs new file mode 100644 index 00000000..481072e2 --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/MsTestHook.cs @@ -0,0 +1,31 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.EFCoreBulk.UnitTest; + +[TestClass] +public class MsTestHook +{ + [AssemblyCleanup] + public static void Cleanup() + { + TestInstanceManager.SetTestEnvironmentVariable(); + var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + } + + [AssemblyInitialize] + public static void Setup(TestContext context) + { + TestInstanceManager.SetTestEnvironmentVariable(); + var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + + db.Database.EnsureCreated(); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/TestInstanceManager.cs b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/TestInstanceManager.cs new file mode 100644 index 00000000..8d6ac967 --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/TestInstanceManager.cs @@ -0,0 +1,34 @@ +using System; +using Lab.EFCoreBulk.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Lab.EFCoreBulk.UnitTest; + +internal class TestInstanceManager +{ + private static IServiceProvider _serviceProvider; + + public static IDbContextFactory EmployeeDbContextFactory => + _serviceProvider.GetService>(); + + static TestInstanceManager() + { + var services = new ServiceCollection(); + ConfigureTestServices(services); + } + + public static void ConfigureTestServices(IServiceCollection services) + { + services.AddAppEnvironment(); + services.AddEntityFramework(); + _serviceProvider = services.BuildServiceProvider(); + } + + public static void SetTestEnvironmentVariable() + { + var option = _serviceProvider.GetService(); + option.EmployeeDbConnectionString = + "Data Source=localhost;Initial Catalog=EmployeeDb;Integrated Security=false;User ID=sa;Password=pass@w0rd1~;MultipleActiveResultSets=True;TrustServerCertificate=True"; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/UnitTest1.cs b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/UnitTest1.cs new file mode 100644 index 00000000..5ecaf99f --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.UnitTest/UnitTest1.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using EFCore.BulkExtensions; +using Lab.EFCoreBulk.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.EFCoreBulk.UnitTest; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void AddRanges() + { + var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); + var toDb = GetEmployees(1000000); + var watch = new Stopwatch(); + watch.Restart(); + + db.AddRange(toDb); + var changeCount = db.SaveChanges(); + + watch.Stop(); + + var count = db.Employees.Count(); + Console.WriteLine($"資料庫存在筆數={count},共花費={watch.Elapsed}"); + } + + [TestMethod] + public void BatchDelete() + { + var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); + var toDb = GetEmployees(10000); + var update = new Employee + { + Id = Guid.NewGuid(), + Age = 10, + CreateBy = "yao", + CreateAt = DateTimeOffset.Now, + Name = "yao", + Remark = "等待更新" + }; + toDb.Add(update); + var config = new BulkConfig { SetOutputIdentity = false, BatchSize = 4000, UseTempDB = true }; + db.BulkInsert(toDb, config); + + var watch = new Stopwatch(); + watch.Restart(); + + db.Employees + .Where(p => p.Id == update.Id) + .BatchDelete(); + + watch.Stop(); + + var count = db.Employees.Count(); + var isExist = db.Employees.Any(p => p.Id == update.Id); + Assert.AreEqual(false, isExist); + Console.WriteLine($"資料庫存在筆數={count},共花費={watch.Elapsed},{update.Id} 資料不存在"); + } + + [TestMethod] + public void BatchUpdate() + { + var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); + var toDb = GetEmployees(10000); + var update = new Employee + { + Id = Guid.NewGuid(), + Age = 10, + CreateBy = "yao", + CreateAt = DateTimeOffset.Now, + Name = "yao", + Remark = "等待更新" + }; + toDb.Add(update); + var config = new BulkConfig { SetOutputIdentity = false, BatchSize = 4000, UseTempDB = true }; + db.BulkInsert(toDb, config); + + var watch = new Stopwatch(); + watch.Restart(); + + db.Employees + .Where(p => p.Id == update.Id) + .BatchUpdate(new Employee { Remark = "Updated" }); + + watch.Stop(); + + var count = db.Employees.Count(); + Console.WriteLine($"資料庫存在筆數={count},共花費={watch.Elapsed}"); + } + + [TestMethod] + public void BulkInsert() + { + var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); + var toDb = GetEmployees(1000000); + + var config = new BulkConfig { SetOutputIdentity = false, BatchSize = 4000, UseTempDB = true }; + + var watch = new Stopwatch(); + watch.Restart(); + + db.BulkInsert(toDb, config); + + watch.Stop(); + + var count = db.Employees.Count(); + Console.WriteLine($"資料庫存在筆數={count},共花費={watch.Elapsed}"); + } + + [TestMethod] + public void BulkRead() + { + var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); + var toDb = GetEmployees(100); + { + var config = new BulkConfig { SetOutputIdentity = false, BatchSize = 4000, UseTempDB = true }; + db.BulkInsert(toDb, config); + } + + var watch = new Stopwatch(); + watch.Restart(); + { + var items = new List + { + new() { Name = "yao1" }, + new() { Name = "yao2" } + }; + var config = new BulkConfig + { + UpdateByProperties = new List + { + nameof(Employee.Name), + }, + UseTempDB = true + }; + db.BulkRead(items, config); + } + + watch.Stop(); + + Console.WriteLine($"共花費={watch.Elapsed}"); + } + + [TestMethod] + public void BulkSaveChanges() + { + var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); + var toDb = GetEmployees(1000); + + db.AddRange(toDb); + + var config = new BulkConfig + { + PropertiesToExclude = new List { "SequenceId" }, + BulkCopyTimeout = 30, + BatchSize = 4000, + UseTempDB = true + }; + + var watch = new Stopwatch(); + watch.Restart(); + + db.BulkSaveChanges(config); + + watch.Stop(); + + var count = db.Employees.Count(); + Console.WriteLine($"資料庫存在筆數={count},共花費={watch.Elapsed}"); + } + + [TestMethod] + public void Contains() + { + var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); + var toDb = GetEmployees(100); + { + var config = new BulkConfig { SetOutputIdentity = false, BatchSize = 4000, UseTempDB = true }; + db.BulkInsert(toDb, config); + } + + var watch = new Stopwatch(); + watch.Restart(); + + var items = new List { "yao1", "yao2" }; + var employees = db.Employees.Where(a => items.Contains(a.Name)).AsNoTracking().ToList(); //SQL IN operator + + watch.Stop(); + + Console.WriteLine($"共花費={watch.Elapsed}"); + } + + [TestCleanup] + public void TestCleanup() + { + CleanData(); + } + + [TestInitialize] + public void TestInitialize() + { + CleanData(); + } + + [TestMethod] + public void TestMethod1() + { + var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); + var id = Guid.NewGuid(); + db.Employees.Add(new Employee + { + Id = id, + Age = 18, + CreateAt = DateTimeOffset.UtcNow, + Name = "yao", + CreateBy = "Sys", + + // Identity = new Identity + // { + // Account = "yao", + // CreateAt = DateTimeOffset.UtcNow, + // CreateBy = "Sys", + // Password = "123456", + // }, + }); + db.SaveChanges(); + } + + [TestMethod] + public void TestMethod2() + { + var host = CreateHostBuilder(null).Start(); + host.Services.GetService>(); + } + + private static void CleanData() + { + using var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); + + // db.Truncate(); + // db.Truncate(); + using var transaction = db.Database.BeginTransaction(); + + db.OrderHistories + .BatchDelete(); + + db.Identities + .BatchDelete(); + + // db.Truncate(); + db.Employees + .BatchDelete(); + + transaction.Commit(); + + // db.Employees + // .Where(p => p.Id != Guid.Empty) + // .BatchDelete(); + // + // while (db.Employees.Any()) + // { + // var deletedCount = db.Employees + // .Where(p => p.Id != Guid.Empty) + // .Take(1000000) + // .BatchDelete(); + // var count = db.Employees.Count(); + // Console.WriteLine($"已刪除 {deletedCount} 筆,剩下 {count} 筆"); + // } + } + + private static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureServices((hostBuilder, services) => + { + TestInstanceManager.ConfigureTestServices(services); + }); + } + + private static List GetEmployees(int totalCount) + { + var employees = Enumerable.Range(0, totalCount) + .Select((x, i) => new Employee + { + Id = Guid.NewGuid(), + + // Id = Guid.NewGuid(), + Age = 10, + + // Age = RandomNumber.Next(1, 100), + CreateBy = "yao", + + // CreateBy = Name.FullName(), + CreateAt = DateTimeOffset.Now, + + // CreateAt = DateTimeOffset.Now, + Name = $"yao{i}" + + // Name = Name.First(), + }).ToList(); + return employees; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.sln b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.sln new file mode 100644 index 00000000..592822e0 --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk.sln @@ -0,0 +1,33 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.EFCoreBulk", "Lab.EFCoreBulk\Lab.EFCoreBulk.csproj", "{3C9700D7-3563-4C7B-9EF4-A7EE512993DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.EFCoreBulk.UnitTest", "Lab.EFCoreBulk.UnitTest\Lab.EFCoreBulk.UnitTest.csproj", "{73344F9D-E263-4EE9-8193-F23343731E56}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{1F776B7D-C182-4DC3-BA57-844657BD9AC0}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3B774B51-B4E4-4F3F-9E1F-43E4A620245A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3C9700D7-3563-4C7B-9EF4-A7EE512993DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C9700D7-3563-4C7B-9EF4-A7EE512993DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C9700D7-3563-4C7B-9EF4-A7EE512993DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C9700D7-3563-4C7B-9EF4-A7EE512993DC}.Release|Any CPU.Build.0 = Release|Any CPU + {73344F9D-E263-4EE9-8193-F23343731E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73344F9D-E263-4EE9-8193-F23343731E56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73344F9D-E263-4EE9-8193-F23343731E56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73344F9D-E263-4EE9-8193-F23343731E56}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3C9700D7-3563-4C7B-9EF4-A7EE512993DC} = {3B774B51-B4E4-4F3F-9E1F-43E4A620245A} + {73344F9D-E263-4EE9-8193-F23343731E56} = {3B774B51-B4E4-4F3F-9E1F-43E4A620245A} + EndGlobalSection +EndGlobal diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/AppDependencyInjectionExtensions.cs b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/AppDependencyInjectionExtensions.cs new file mode 100644 index 00000000..3a0becd7 --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/AppDependencyInjectionExtensions.cs @@ -0,0 +1,52 @@ +using Lab.EFCoreBulk.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.EFCoreBulk; + +public static class AppDependencyInjectionExtensions +{ + public static void AddAppEnvironment(this IServiceCollection services) + { + services.AddLogging(builder => + { + builder.AddConsole(); + }); + services.AddSingleton(); + } + + public static void AddEntityFramework(this IServiceCollection services) + { + services.AddPooledDbContextFactory((provider, optionsBuilder) => + { + var option = provider.GetService(); + var connectionString = option.EmployeeDbConnectionString; + var loggerFactory = provider.GetService(); + optionsBuilder.UseSqlServer(connectionString) + .UseLoggerFactory(loggerFactory) + ; + }); + + ; + + // services.AddPooledDbContextFactory((provider, options) => + // { + // var option = provider.GetService(); + // var loggerFactory = provider.GetService(); + // options.UseNpgsql( + // option.MemberDbConnectionString, //只會呼叫一次 + // builder => + // builder.EnableRetryOnFailure( + // 10, + // TimeSpan.FromSeconds(30), + // new List { "57P01" })) + // + // // .UseLazyLoadingProxies() + // .UseSnakeCaseNamingConvention() + // .UseLoggerFactory(loggerFactory) + // ; + // }); + } + +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/AppEnvironmentOption.cs b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/AppEnvironmentOption.cs new file mode 100644 index 00000000..ef209511 --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/AppEnvironmentOption.cs @@ -0,0 +1,31 @@ +namespace Lab.EFCoreBulk; + +public class AppEnvironmentOption +{ + public string EmployeeDbConnectionString + { + get + { + if (string.IsNullOrWhiteSpace(this._employeeDbConnectionString)) + { + this._employeeDbConnectionString = + EnvironmentAssistant.GetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR); + } + + return this._employeeDbConnectionString; + } + set + { + this._employeeDbConnectionString = value; + Environment.SetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR, value); + } + } + + private string _employeeDbConnectionString; + private readonly string EMPLOYEE_DB_CONN_STR = "EMPLOYEE_DB_CONNECTION_STR"; + + public void Initial() + { + var memberDbConnectionString = this.EmployeeDbConnectionString; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/Employee.cs b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/Employee.cs new file mode 100644 index 00000000..b2fec6b5 --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/Employee.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.EFCoreBulk.EntityModel +{ + [Table("Employee")] + public class Employee + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Id { get; set; } + + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + public string Remark { get; set; } + + [Required] + public DateTimeOffset CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + public virtual Identity Identity { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/EmployeeDbContext.cs b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..e34a7e7b --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.InMemory.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; + +namespace Lab.EFCoreBulk.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = { false }; + + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identities { get; set; } + + public virtual DbSet OrderHistories { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + if (s_migrated[0]) + { + return; + } + + lock (s_migrated) + { + if (s_migrated[0] == false) + { + var memoryOptions = options.FindExtension(); + + if (memoryOptions == null) + { + var sqlOptions = options.FindExtension(); + if (sqlOptions != null) + { + Console.WriteLine( + $"EmployeeDbContext of connection string be '{sqlOptions.ConnectionString}'"); + this.Database.Migrate(); + } + } + + s_migrated[0] = true; + } + } + } + + //管理索引 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(p => + { + p.HasKey(e => e.Id) + .IsClustered(false); + + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + + p.Property(p => p.Remark) + .IsRequired(false) + ; + }); + + modelBuilder.Entity(p => + { + p.HasKey(e => e.Employee_Id) + .IsClustered(false); + + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + }); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/Identity.cs b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/Identity.cs new file mode 100644 index 00000000..47575538 --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/Identity.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.EFCoreBulk.EntityModel +{ + [Table("Identity")] + public class Identity + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Employee_Id { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Remark { get; set; } + + [Required] + public DateTimeOffset CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + [ForeignKey("Employee_Id")] + public virtual Employee Employee { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/OrderHistory.cs b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/OrderHistory.cs new file mode 100644 index 00000000..c1de6f7b --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EntityModel/OrderHistory.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.EFCoreBulk.EntityModel +{ + [Table("OrderHistory")] + public class OrderHistory + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public Guid? Employee_Id { get; set; } + + public string Remark { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Product_Id { get; set; } + + public string Product_Name { get; set; } + + [Required] + public DateTime CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EnvironmentAssistant.cs b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EnvironmentAssistant.cs new file mode 100644 index 00000000..942a8c40 --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/EnvironmentAssistant.cs @@ -0,0 +1,15 @@ +namespace Lab.EFCoreBulk; + +public class EnvironmentAssistant +{ + public static string GetEnvironmentVariable(string key) + { + var result = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrWhiteSpace(result)) + { + throw new Exception($"the key '{key}' not exist in environment variable"); + } + + return result; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/Lab.EFCoreBulk.csproj b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/Lab.EFCoreBulk.csproj new file mode 100644 index 00000000..39ae089b --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/Lab.EFCoreBulk/Lab.EFCoreBulk.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/ORM/EFCore/Lab.EFCoreBulk/docker-compose.yml b/ORM/EFCore/Lab.EFCoreBulk/docker-compose.yml new file mode 100644 index 00000000..8ecb1567 --- /dev/null +++ b/ORM/EFCore/Lab.EFCoreBulk/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.8" + +services: + db-sql: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=pass@w0rd1~ + ports: + - 1433:1433 \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/EmployeeRepositoryUnitTests.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/EmployeeRepositoryUnitTests.cs new file mode 100644 index 00000000..647c94ae --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/EmployeeRepositoryUnitTests.cs @@ -0,0 +1,482 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Lab.DAL.DomainModel.Employee; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.DAL.TestProject +{ + [TestClass] + public class EmployeeRepositoryUnitTests + { + private static readonly DbContextOptions s_employeeContextOptions; + private static readonly string TestDbConnectionString1 = "Data Source=Lab.DAL.TestProject.db"; + private static readonly string TestDbConnectionString2 = "Data Source=Lab.DAL.Injection.db"; + + static EmployeeRepositoryUnitTests() + { + s_employeeContextOptions = CreateDbContextOptions(); + } + + [AssemblyCleanup] + public static void AssemblyCleanup() + { + //刪除測試資料庫 + Console.WriteLine("AssemblyCleanup"); + + using var db1 = new TestEmployeeDbContext(TestDbConnectionString1); + db1.Database.EnsureDeleted(); + + using var db2 = new TestEmployeeDbContext(TestDbConnectionString2); + db2.Database.EnsureDeleted(); + } + + [AssemblyInitialize] + public static void AssemblyInitialize(TestContext context) + { + //刪除測試資料庫 + Console.WriteLine("AssemblyInitialize"); + using var db1 = new TestEmployeeDbContext(TestDbConnectionString1); + db1.Database.EnsureDeleted(); + + using var db2 = new TestEmployeeDbContext(TestDbConnectionString2); + db2.Database.EnsureDeleted(); + + // //建立測試資料庫 + // db.Database.Migrate(); + } + + [ClassCleanup] + public static void ClassCleanup() + { + //刪除測試資料表 + Console.WriteLine("ClassCleanup"); + + // DeleteTestDataRow(); + } + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + //刪除測試資料表 + Console.WriteLine("ClassInitialize"); + + // DeleteTestDataRow(); + } + + [TestMethod] + public void 操作真實資料庫_手動取得Repository執行個體() + { + //arrange + DefaultDbContextManager.Now = new DateTime(1900, 1, 1); + DefaultDbContextManager.SetPhysicalDatabase(); + + var repository = new EmployeeRepository(); + + repository.NewAsync(new NewRequest + { + Account = "yao", + Password = "123456", + Name = "余小章", + Age = 18, + Remark = "測試案例,持續航向偉大航道" + }, "TestUser").Wait(); + + using var db = new TestEmployeeDbContext(TestDbConnectionString1); + var id = db.Employees.FirstOrDefault(p => p.Name == "余小章").Id; + + //act + var count = repository.InsertLogAsync(new InsertOrderRequest + { + Employee_Id = id, + Product_Id = "A001", + Product_Name = "羅技滑鼠", + Remark = "測試案例,持續航向偉大航道" + }, "TestUser").Result; + + //assert + Assert.AreEqual(1, count); + } + + [TestMethod] + public void 操作真實資料庫_手動實例化EmployeeDbContext() + { + var contextOptions = CreateDbContextOptions(); + using var dbContext = new EmployeeDbContext(contextOptions); + var id = Guid.NewGuid().ToString(); + dbContext.Employees.Add(new Employee() + { + Age = 18, + Id = id, + CreateAt = DateTime.Now, + CreateBy = "test", + Name = "yao" + }); + dbContext.SaveChanges(); + + var actual = dbContext.Employees.AsNoTracking().FirstOrDefault(p => p.Id == id); + Assert.AreEqual(18, actual.Age); + Assert.AreEqual("yao", actual.Name); + } + + [TestMethod] + public void 操作真實資料庫_由Host註冊EmployeeDbContext() + { + //arrange + var builder = Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddDbContext( + (provider, builder) => + { + var config = + provider.GetService(); + var connectionString = + config.GetConnectionString("DefaultConnection"); + var loggerFactory = provider.GetService(); + builder.UseSqlite(connectionString) + .UseLoggerFactory(loggerFactory) + ; + }); + }); + var host = builder.Build(); + + var dbContextOptions = host.Services.GetService>(); + using var dbContext = host.Services.GetService(); + + //act + var id = Guid.NewGuid().ToString(); + var now = DateTime.Now; + dbContext.Employees.Add(new Employee() + { + Id = id, + Name = "余小章", + Age = 18, + CreateAt = now, + CreateBy = "test user" + }); + dbContext.Identities.Add(new Identity() + { + Employee_Id = id, + Account = "yao", + Password = "123456", + CreateAt = now, + CreateBy = "test user" + }); + var count = dbContext.SaveChanges(); + + //assert + Assert.AreEqual(2, count); + + using var db = new EmployeeDbContext(dbContextOptions); + + var actual = db.Employees + .Include(p => p.Identity) + .AsNoTracking() + .FirstOrDefault(); + + Assert.AreEqual(actual.Name, "余小章"); + Assert.AreEqual(actual.Age, 18); + Assert.AreEqual(actual.Identity.Account, "yao"); + Assert.AreEqual(actual.Identity.Password, "123456"); + } + + [TestMethod] + public void 操作真實資料庫_由Host註冊EmployeeDbContextPool() + { + //arrange + var builder = Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddDbContextPool( + (provider, builder) => + { + var config = + provider.GetService(); + var connectionString = + config.GetConnectionString("DefaultConnection"); + var loggerFactory = provider.GetService(); + builder.UseSqlite(connectionString) + .UseLoggerFactory(loggerFactory) + ; + }, 64); + }); + var host = builder.Build(); + + var dbContextOptions = host.Services.GetService>(); + using var dbContext = host.Services.GetService(); + + //act + var id = Guid.NewGuid().ToString(); + var now = DateTime.Now; + dbContext.Employees.Add(new Employee() + { + Id = id, + Name = "余小章", + Age = 18, + CreateAt = now, + CreateBy = "test user" + }); + dbContext.Identities.Add(new Identity() + { + Employee_Id = id, + Account = "yao", + Password = "123456", + CreateAt = now, + CreateBy = "test user" + }); + var count = dbContext.SaveChanges(); + + //assert + Assert.AreEqual(2, count); + + using var db = new EmployeeDbContext(dbContextOptions); + + var actual = db.Employees + .Include(p => p.Identity) + .AsNoTracking() + .FirstOrDefault(); + + Assert.AreEqual(actual.Name, "余小章"); + Assert.AreEqual(actual.Age, 18); + Assert.AreEqual(actual.Identity.Account, "yao"); + Assert.AreEqual(actual.Identity.Password, "123456"); + } + + [TestMethod] + public void 操作真實資料庫_由Host註冊EmployeeDbContextFactory() + { + //arrange + var builder = Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddDbContextFactory( + (provider, builder) => + { + var config = + provider.GetService(); + var connectionString = + config.GetConnectionString("DefaultConnection"); + var loggerFactory = provider.GetService(); + builder.UseSqlite(connectionString) + .UseLoggerFactory(loggerFactory) + ; + }); + }); + var host = builder.Build(); + + var dbContextOptions = host.Services.GetService>(); + var dbContextFactory = host.Services.GetService>(); + using var dbContext = dbContextFactory.CreateDbContext(); + + //act + var id = Guid.NewGuid().ToString(); + var now = DateTime.Now; + dbContext.Employees.Add(new Employee() + { + Id = id, + Name = "余小章", + Age = 18, + CreateAt = now, + CreateBy = "test user" + }); + dbContext.Identities.Add(new Identity() + { + Employee_Id = id, + Account = "yao", + Password = "123456", + CreateAt = now, + CreateBy = "test user" + }); + var count = dbContext.SaveChanges(); + + //assert + Assert.AreEqual(2, count); + + using var db = new EmployeeDbContext(dbContextOptions); + + var actual = db.Employees + .Include(p => p.Identity) + .AsNoTracking() + .FirstOrDefault(); + + Assert.AreEqual(actual.Name, "余小章"); + Assert.AreEqual(actual.Age, 18); + Assert.AreEqual(actual.Identity.Account, "yao"); + Assert.AreEqual(actual.Identity.Password, "123456"); + } + + [TestMethod] + public void 操作真實資料庫_由Host註冊EmployeeDbContextPoolFactory() + { + //arrange + var builder = Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddPooledDbContextFactory( + (provider, builder) => + { + var config = + provider.GetService(); + var connectionString = + config.GetConnectionString("DefaultConnection"); + var loggerFactory = provider.GetService(); + builder.UseSqlite(connectionString) + .UseLoggerFactory(loggerFactory) + ; + }, 64); + }); + var host = builder.Build(); + + var dbContextOptions = host.Services.GetService>(); + var dbContextFactory = host.Services.GetService>(); + using var dbContext = dbContextFactory.CreateDbContext(); + + //act + var id = Guid.NewGuid().ToString(); + var now = DateTime.Now; + dbContext.Employees.Add(new Employee() + { + Id = id, + Name = "余小章", + Age = 18, + CreateAt = now, + CreateBy = "test user" + }); + dbContext.Identities.Add(new Identity() + { + Employee_Id = id, + Account = "yao", + Password = "123456", + CreateAt = now, + CreateBy = "test user" + }); + var count = dbContext.SaveChanges(); + + //assert + Assert.AreEqual(2, count); + + using var db = new EmployeeDbContext(dbContextOptions); + + var actual = db.Employees + .Include(p => p.Identity) + .AsNoTracking() + .FirstOrDefault(); + + Assert.AreEqual(actual.Name, "余小章"); + Assert.AreEqual(actual.Age, 18); + Assert.AreEqual(actual.Identity.Account, "yao"); + Assert.AreEqual(actual.Identity.Password, "123456"); + } + + [TestMethod] + public void 操作真實資料庫_預設EmployeeDbContext() + { + //arrange + DefaultDbContextManager.Now = new DateTime(1900, 1, 1); + DefaultDbContextManager.SetPhysicalDatabase(); + + var builder = Host.CreateDefaultBuilder() + .ConfigureServices(services => { services.AddSingleton(); }); + var host = builder.Build(); + + var repository = host.Services.GetService(); + + //act + var count = repository.NewAsync(new NewRequest + { + Account = "yao", + Password = "123456", + Name = "余小章", + Age = 18, + }, "TestUser").Result; + + //assert + Assert.AreEqual(2, count); + using var db = new TestEmployeeDbContext(TestDbConnectionString1); + + var actual = db.Employees + .Include(p => p.Identity) + .AsNoTracking() + .FirstOrDefault(); + + Assert.AreEqual(actual.Name, "余小章"); + Assert.AreEqual(actual.Age, 18); + Assert.AreEqual(actual.Identity.Account, "yao"); + Assert.AreEqual(actual.Identity.Password, "123456"); + } + + [TestMethod] + public void 操作記憶體() + { + DefaultDbContextManager.Now = new DateTime(1900, 1, 1); + DefaultDbContextManager.SetMemoryDatabase(); + + var builder = Host.CreateDefaultBuilder() + .ConfigureServices(services => { services.AddSingleton(); }); + var host = builder.Build(); + + var repository = host.Services.GetService(); + var count = repository.NewAsync(new NewRequest(), "TestUser").Result; + Assert.AreEqual(2, count); + } + + private static DbContextOptions CreateDbContextOptions() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + + var configRoot = configBuilder.Build(); + var connectionString = configRoot.GetConnectionString("DefaultConnection"); + + var loggerFactory = LoggerFactory.Create(builder => + { + builder + + //.AddFilter("Microsoft", LogLevel.Warning) + //.AddFilter("System", LogLevel.Warning) + .AddFilter("Lab.DAL", LogLevel.Debug) + .AddConsole() + ; + }); + return new DbContextOptionsBuilder() + .UseSqlite(connectionString) + .UseLoggerFactory(loggerFactory) + .Options; + } + + private static void DeleteTestDataRow() + { + var dbContextOptions = s_employeeContextOptions; + using var db = new EmployeeDbContext(dbContextOptions); + var deleteCommand = GetDeleteAllRecordCommand(); + db.Database.ExecuteSqlRaw(deleteCommand); + } + + private static string GetDeleteAllRecordCommand() + { + var sql = @" +-- disable referential integrity +EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL' + + +EXEC sp_MSForEachTable 'DELETE FROM ?' + + +-- enable referential integrity again +EXEC sp_MSForEachTable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL' +"; + + return sql; + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/Lab.DAL.TestProject.csproj b/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/Lab.DAL.TestProject.csproj new file mode 100644 index 00000000..29435055 --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/Lab.DAL.TestProject.csproj @@ -0,0 +1,29 @@ + + + + net5.0 + bin + false + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/TestEmployeeDbContext.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/TestEmployeeDbContext.cs new file mode 100644 index 00000000..74a2e32d --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/TestEmployeeDbContext.cs @@ -0,0 +1,38 @@ +using System; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; + +namespace Lab.DAL.TestProject +{ + public class TestEmployeeDbContext : DbContext + { + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identities { get; set; } + + public virtual DbSet OrderHistories { get; set; } + + private readonly string _connectionString; + + public TestEmployeeDbContext(string connectionString) + { + this._connectionString = connectionString; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + var connectionString = this._connectionString; + if (optionsBuilder.IsConfigured == false) + { + Console.WriteLine($"設定連線字串:{connectionString}"); + optionsBuilder.UseSqlite(connectionString); + } + } + + //管理索引 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/appsettings.json b/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/appsettings.json new file mode 100644 index 00000000..a83c326c --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL.TestProject/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=Lab.DAL.TestProject.db" + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/DefaultDbContextManager.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/DefaultDbContextManager.cs new file mode 100644 index 00000000..4a70778a --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/DefaultDbContextManager.cs @@ -0,0 +1,141 @@ +using System; +using System.IO; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.DAL +{ + internal class DefaultDbContextManager + { + private static readonly Lazy s_serviceProviderLazy; + private static readonly Lazy s_configurationLazy; + private static readonly ILoggerFactory s_loggerFactory; + + private static readonly ServiceCollection s_services; + private static ServiceProvider s_serviceProvider; + private static IConfiguration s_configuration; + private static DateTime? s_now; + + public static DateTime Now + { + get + { + if (s_now == null) + { + return DateTime.UtcNow; + } + + return s_now.Value; + } + set => s_now = value; + } + + public static ServiceProvider ServiceProvider + { + get + { + if (s_serviceProvider == null) + { + s_serviceProvider = s_serviceProviderLazy.Value; + } + + return s_serviceProvider; + } + set => s_serviceProvider = value; + } + + public static IConfiguration Configuration + { + get + { + if (s_configuration == null) + { + s_configuration = s_configurationLazy.Value; + } + + return s_configuration; + } + set => s_configuration = value; + } + + static DefaultDbContextManager() + { + s_services = new ServiceCollection(); + + s_serviceProviderLazy = + new Lazy(() => + { + var services = s_services; + services.AddDbContextFactory(ApplyConfigurePhysical); + return services.BuildServiceProvider(); + }); + s_configurationLazy + = new Lazy(() => + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + return configBuilder.Build(); + }); + s_loggerFactory = LoggerFactory.Create(builder => + { + builder + + //.AddFilter("Microsoft", LogLevel.Warning) + //.AddFilter("System", LogLevel.Warning) + .AddFilter("Lab.DAL", LogLevel.Debug) + .AddConsole() + ; + }); + } + + public static T GetInstance() + { + return ServiceProvider.GetService(); + } + + public static void SetMemoryDatabase() where TContext : DbContext + { + var services = s_services; + + services.Clear(); + services.AddDbContextFactory(ApplyConfigureMemory); + ServiceProvider = services.BuildServiceProvider(); + } + + public static void SetPhysicalDatabase() where TContext : DbContext + { + var services = s_services; + + services.Clear(); + services.AddDbContextFactory(ApplyConfigurePhysical); + ServiceProvider = services.BuildServiceProvider(); + } + + private static void ApplyConfigureMemory(IServiceProvider provider, + DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseInMemoryDatabase("Lab.DAL") + .UseLoggerFactory(s_loggerFactory) + ; + } + + private static void ApplyConfigurePhysical(IServiceProvider provider, + DbContextOptionsBuilder optionsBuilder) + { + var config = provider.GetService(); + if (config == null) + { + config = Configuration; + } + + var connectionString = config.GetConnectionString("DefaultConnection"); + optionsBuilder.UseSqlite(connectionString) + .UseLoggerFactory(s_loggerFactory) + ; + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/DomainModel/Employee/InsertOrderRequest.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/DomainModel/Employee/InsertOrderRequest.cs new file mode 100644 index 00000000..52598da3 --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/DomainModel/Employee/InsertOrderRequest.cs @@ -0,0 +1,13 @@ +namespace Lab.DAL.DomainModel.Employee +{ + public class InsertOrderRequest + { + public string Employee_Id { get; set; } + + public string Product_Id { get; set; } + + public string Product_Name { get; set; } + + public string Remark { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/DomainModel/Employee/NewRequest.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/DomainModel/Employee/NewRequest.cs new file mode 100644 index 00000000..490f696e --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/DomainModel/Employee/NewRequest.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace Lab.DAL.DomainModel.Employee +{ + public class NewRequest + { + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + public string Remark { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/EmployeeContextFactory.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/EmployeeContextFactory.cs new file mode 100644 index 00000000..44a7b638 --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/EmployeeContextFactory.cs @@ -0,0 +1,27 @@ +using System; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace Lab.DAL +{ + public class EmployeeContextFactory : IDesignTimeDbContextFactory + { + public EmployeeDbContext CreateDbContext(string[] args) + { + Console.WriteLine("EmployeeContextFactory - 由設計工具產生 Database,初始化 DbContextOptionsBuilder"); + + var config = DefaultDbContextManager.Configuration; + var connectionString = config.GetConnectionString("DefaultConnection"); + + Console.WriteLine($"EmployeeContextFactory - 讀取 appsettings.json 檔案的讀取連線字串為:{connectionString}"); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlite(connectionString); + Console.WriteLine($"EmployeeContextFactory - DbContextOptionsBuilder 設定完成"); + + return new EmployeeDbContext(optionsBuilder.Options); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/EmployeeRepository.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/EmployeeRepository.cs new file mode 100644 index 00000000..cd607a64 --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/EmployeeRepository.cs @@ -0,0 +1,121 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Lab.DAL.DomainModel.Employee; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; + +namespace Lab.DAL +{ + public interface IEmployeeRepository + { + Task InsertLogAsync(InsertOrderRequest request, + string accessId, + CancellationToken cancel = default); + + Task NewAsync(NewRequest request, + string accessId, + CancellationToken cancel = default); + } + + public class EmployeeRepository : IEmployeeRepository + { + internal IDbContextFactory DbContextFactory + { + get + { + if (this._dbContextFactory == null) + { + return DefaultDbContextManager.GetInstance>(); + } + + return this._dbContextFactory; + } + set => this._dbContextFactory = value; + } + + internal EmployeeDbContext EmployeeDbContext + { + get + { + if (this._employeeDbContext == null) + { + return this.DbContextFactory.CreateDbContext(); + } + + return this._employeeDbContext; + } + set => this._employeeDbContext = value; + } + + internal DateTime Now + { + get + { + if (this._now == null) + { + return DefaultDbContextManager.Now; + } + + return this._now.Value; + } + set => this._now = value; + } + + private IDbContextFactory _dbContextFactory; + private EmployeeDbContext _employeeDbContext; + private DateTime? _now; + + public async Task InsertLogAsync(InsertOrderRequest request, + string accessId, + CancellationToken cancel = default) + { + await using var dbContext = this.EmployeeDbContext; + + var toDbOrderHistory = new OrderHistory + { + Id = Guid.NewGuid().ToString(), + Employee_Id = request.Employee_Id, + Product_Id = request.Product_Id, + Product_Name = request.Product_Id, + CreateAt = this.Now, + CreateBy = accessId, + Remark = request.Remark, + }; + + await dbContext.OrderHistories.AddAsync(toDbOrderHistory, cancel); + return await dbContext.SaveChangesAsync(cancel); + } + + public async Task NewAsync(NewRequest request, + string accessId, + CancellationToken cancel = default) + { + using var dbContext = this.EmployeeDbContext; + var id = Guid.NewGuid().ToString(); + var employeeToDb = new Employee + { + Id = id, + Name = request.Name, + Age = request.Age, + Remark = request.Remark, + CreateAt = this.Now, + CreateBy = accessId + }; + + var identityToDb = new Identity + { + Account = request.Account, + Password = request.Password, + Remark = request.Remark, + Employee = employeeToDb, + CreateAt = this.Now, + CreateBy = accessId + }; + + employeeToDb.Identity = identityToDb; + await dbContext.Employees.AddAsync(employeeToDb, cancel); + return await dbContext.SaveChangesAsync(cancel); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/Employee.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/Employee.cs new file mode 100644 index 00000000..ca5252e6 --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/Employee.cs @@ -0,0 +1,29 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.DAL.EntityModel +{ + [Table("Employee")] + public class Employee + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Id { get; set; } + + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + public string Remark { get; set; } + + [Required] + public DateTime CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + public virtual Identity Identity { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/EmployeeDbContext.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..c8d3832f --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.InMemory.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Sqlite.Infrastructure.Internal; + +namespace Lab.DAL.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = {false}; + + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identities { get; set; } + + public virtual DbSet OrderHistories { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + if (s_migrated[0]) + { + return; + } + + lock (s_migrated) + { + if (s_migrated[0] == false) + { + var memoryOptions = options.FindExtension(); + + if (memoryOptions == null) + { + var sqliteOptionsExtension = options.FindExtension(); + + if (sqliteOptionsExtension != null) + { + Console.WriteLine($"EmployeeDbContext 的連線字串為:{sqliteOptionsExtension.ConnectionString},執行 Migration"); + } + + this.Database.Migrate(); + } + + s_migrated[0] = true; + } + } + } + + // 給 Migration CLI 使用 + // 建構函數配置失敗才需要以下處理 + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // var connectionString = + // "Server=(localdb)\\mssqllocaldb;Database=Lab.DAL.UnitTest;Trusted_Connection=True;MultipleActiveResultSets=true"; + // + // // var connectionString = this._connectionString; + // if (optionsBuilder.IsConfigured == false) + // { + // Console.WriteLine("OnConfiguring"); + // optionsBuilder.UseSqlite(connectionString); + // } + } + + //管理索引 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + Console.WriteLine("設定資料表定義"); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/Identity.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/Identity.cs new file mode 100644 index 00000000..5ab55f04 --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/Identity.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.DAL.EntityModel +{ + [Table("Identity")] + public class Identity + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Employee_Id { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + public string Remark { get; set; } + + [Required] + public DateTime CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + [ForeignKey("Employee_Id")] + public virtual Employee Employee { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/OrderHistory.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/OrderHistory.cs new file mode 100644 index 00000000..7a77ca2e --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/EntityModel/OrderHistory.cs @@ -0,0 +1,28 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.DAL.EntityModel +{ + [Table("OrderHistory")] + public class OrderHistory + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Id { get; set; } + + public string Employee_Id { get; set; } + + public string Remark { get; set; } + + public string Product_Id { get; set; } + + public string Product_Name { get; set; } + + [Required] + public DateTime CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/Lab.DAL.csproj b/ORM/EFCore/Lab.SQLite/Lab.DAL/Lab.DAL.csproj new file mode 100644 index 00000000..e7762bff --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/Lab.DAL.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + bin + bin\Lab.DAL.xml + + + + + + + + + + + + <_Parameter1>Lab.DAL.TestProject + + + + + Always + + + diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/Migrations/20210418023303_InitialCreate.Designer.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/Migrations/20210418023303_InitialCreate.Designer.cs new file mode 100644 index 00000000..55cf0c0e --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/Migrations/20210418023303_InitialCreate.Designer.cs @@ -0,0 +1,123 @@ +// +using System; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Lab.DAL.Migrations +{ + [DbContext(typeof(EmployeeDbContext))] + [Migration("20210418023303_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.5"); + + modelBuilder.Entity("Lab.DAL.EntityModel.Employee", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Age") + .HasColumnType("INTEGER"); + + b.Property("CreateAt") + .HasColumnType("TEXT"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Remark") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Employee"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Identity", b => + { + b.Property("Employee_Id") + .HasColumnType("TEXT"); + + b.Property("Account") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreateAt") + .HasColumnType("TEXT"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Remark") + .HasColumnType("TEXT"); + + b.HasKey("Employee_Id"); + + b.ToTable("Identity"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.OrderHistory", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreateAt") + .HasColumnType("TEXT"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Employee_Id") + .HasColumnType("TEXT"); + + b.Property("Product_Id") + .HasColumnType("TEXT"); + + b.Property("Product_Name") + .HasColumnType("TEXT"); + + b.Property("Remark") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("OrderHistory"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Identity", b => + { + b.HasOne("Lab.DAL.EntityModel.Employee", "Employee") + .WithOne("Identity") + .HasForeignKey("Lab.DAL.EntityModel.Identity", "Employee_Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Employee", b => + { + b.Navigation("Identity"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/Migrations/20210418023303_InitialCreate.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/Migrations/20210418023303_InitialCreate.cs new file mode 100644 index 00000000..1e11c0c3 --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/Migrations/20210418023303_InitialCreate.cs @@ -0,0 +1,78 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Lab.DAL.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Employee", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Age = table.Column(type: "INTEGER", nullable: true), + Remark = table.Column(type: "TEXT", nullable: true), + CreateAt = table.Column(type: "TEXT", nullable: false), + CreateBy = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Employee", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OrderHistory", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Employee_Id = table.Column(type: "TEXT", nullable: true), + Remark = table.Column(type: "TEXT", nullable: true), + Product_Id = table.Column(type: "TEXT", nullable: true), + Product_Name = table.Column(type: "TEXT", nullable: true), + CreateAt = table.Column(type: "TEXT", nullable: false), + CreateBy = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderHistory", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Identity", + columns: table => new + { + Employee_Id = table.Column(type: "TEXT", nullable: false), + Account = table.Column(type: "TEXT", nullable: false), + Password = table.Column(type: "TEXT", nullable: false), + Remark = table.Column(type: "TEXT", nullable: true), + CreateAt = table.Column(type: "TEXT", nullable: false), + CreateBy = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Identity", x => x.Employee_Id); + table.ForeignKey( + name: "FK_Identity_Employee_Employee_Id", + column: x => x.Employee_Id, + principalTable: "Employee", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Identity"); + + migrationBuilder.DropTable( + name: "OrderHistory"); + + migrationBuilder.DropTable( + name: "Employee"); + } + } +} diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/Migrations/EmployeeDbContextModelSnapshot.cs b/ORM/EFCore/Lab.SQLite/Lab.DAL/Migrations/EmployeeDbContextModelSnapshot.cs new file mode 100644 index 00000000..7d2d01a4 --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/Migrations/EmployeeDbContextModelSnapshot.cs @@ -0,0 +1,121 @@ +// +using System; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Lab.DAL.Migrations +{ + [DbContext(typeof(EmployeeDbContext))] + partial class EmployeeDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.5"); + + modelBuilder.Entity("Lab.DAL.EntityModel.Employee", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Age") + .HasColumnType("INTEGER"); + + b.Property("CreateAt") + .HasColumnType("TEXT"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Remark") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Employee"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Identity", b => + { + b.Property("Employee_Id") + .HasColumnType("TEXT"); + + b.Property("Account") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreateAt") + .HasColumnType("TEXT"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Remark") + .HasColumnType("TEXT"); + + b.HasKey("Employee_Id"); + + b.ToTable("Identity"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.OrderHistory", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreateAt") + .HasColumnType("TEXT"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Employee_Id") + .HasColumnType("TEXT"); + + b.Property("Product_Id") + .HasColumnType("TEXT"); + + b.Property("Product_Name") + .HasColumnType("TEXT"); + + b.Property("Remark") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("OrderHistory"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Identity", b => + { + b.HasOne("Lab.DAL.EntityModel.Employee", "Employee") + .WithOne("Identity") + .HasForeignKey("Lab.DAL.EntityModel.Identity", "Employee_Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Employee", b => + { + b.Navigation("Identity"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ORM/EFCore/Lab.SQLite/Lab.DAL/appsettings.json b/ORM/EFCore/Lab.SQLite/Lab.DAL/appsettings.json new file mode 100644 index 00000000..be206a85 --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.DAL/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=bin\\Lab.DAL.db" + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.SQLite/Lab.SQLite.sln b/ORM/EFCore/Lab.SQLite/Lab.SQLite.sln new file mode 100644 index 00000000..049ab624 --- /dev/null +++ b/ORM/EFCore/Lab.SQLite/Lab.SQLite.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.DAL", "Lab.DAL\Lab.DAL.csproj", "{663838CE-1F72-483A-B981-8CCE685911C3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.DAL.TestProject", "Lab.DAL.TestProject\Lab.DAL.TestProject.csproj", "{EFF09A2D-3B94-4BCF-8A38-18186E64F25F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {663838CE-1F72-483A-B981-8CCE685911C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {663838CE-1F72-483A-B981-8CCE685911C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {663838CE-1F72-483A-B981-8CCE685911C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {663838CE-1F72-483A-B981-8CCE685911C3}.Release|Any CPU.Build.0 = Release|Any CPU + {EFF09A2D-3B94-4BCF-8A38-18186E64F25F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFF09A2D-3B94-4BCF-8A38-18186E64F25F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFF09A2D-3B94-4BCF-8A38-18186E64F25F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFF09A2D-3B94-4BCF-8A38-18186E64F25F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.Biz/IEmployeeRepository.cs b/ORM/EFCore/Lab.VirtualDb/Lab.Biz/IEmployeeRepository.cs new file mode 100644 index 00000000..5f0cc7f0 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.Biz/IEmployeeRepository.cs @@ -0,0 +1,8 @@ +using System; + +namespace Lab.Biz +{ + public interface IEmployeeRepository + { + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.Biz/Lab.Biz.csproj b/ORM/EFCore/Lab.VirtualDb/Lab.Biz/Lab.Biz.csproj new file mode 100644 index 00000000..cbfa5815 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.Biz/Lab.Biz.csproj @@ -0,0 +1,7 @@ + + + + net5.0 + + + diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/EmployeeRepositoryUnitTests.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/EmployeeRepositoryUnitTests.cs new file mode 100644 index 00000000..860a0a25 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/EmployeeRepositoryUnitTests.cs @@ -0,0 +1,270 @@ +using System; +using System.IO; +using System.Linq; +using Lab.DAL.DomainModel.Employee; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.DAL.TestProject +{ + [TestClass] + public class EmployeeRepositoryUnitTests + { + private static readonly DbContextOptions s_employeeContextOptions; + public static string TestDbConnectionString; + + static EmployeeRepositoryUnitTests() + { + s_employeeContextOptions = CreateDbContextOptions(); + + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + + var configRoot = configBuilder.Build(); + TestDbConnectionString = configRoot.GetConnectionString("DefaultConnection"); + } + + [AssemblyCleanup] + public static void AssemblyCleanup() + { + //刪除測試資料庫 + Console.WriteLine("AssemblyCleanup"); + + using var db = new TestEmployeeDbContext(TestDbConnectionString); + db.Database.EnsureDeleted(); + } + + [AssemblyInitialize] + public static void AssemblyInitialize(TestContext context) + { + //刪除測試資料庫 + Console.WriteLine("AssemblyInitialize"); + using var db = new TestEmployeeDbContext(TestDbConnectionString); + db.Database.EnsureDeleted(); + } + + [ClassCleanup] + public static void ClassCleanup() + { + //刪除測試資料表 + Console.WriteLine("ClassCleanup"); + DeleteTestDataRow(); + } + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + //刪除測試資料表 + Console.WriteLine("ClassInitialize"); + DeleteTestDataRow(); + } + + [TestMethod] + public void 操作真實資料庫_手動取得Repository執行個體() + { + //arrange + DefaultDbContextManager.Now = new DateTime(1900, 1, 1); + DefaultDbContextManager.SetPhysicalDatabase(); + + var repository = new EmployeeRepository(); + + // var id = Guid.NewGuid(); + repository.NewAsync(new NewRequest + { + Account = "yao", + Password = "123456", + Name = "余小章", + Age = 18, + Remark = "測試案例,持續航向偉大航道" + }, "TestUser").Wait(); + using var db = new EmployeeDbContext(s_employeeContextOptions); + var id = db.Employees.FirstOrDefault(p => p.Name == "余小章").Id; + + //act + var count = repository.InsertLogAsync(new InsertOrderRequest + { + Employee_Id = id, + Product_Id = "A001", + Product_Name = "羅技滑鼠", + Remark = "測試案例,持續航向偉大航道" + }, "TestUser").Result; + + //assert + Assert.AreEqual(1, count); + + using var db1 = new TestEmployeeDbContext(TestDbConnectionString); + + var actual = db1.OrderHistories + .AsNoTracking() + .FirstOrDefault(); + + Assert.AreEqual("A001", actual.Product_Id); + Assert.AreEqual("羅技滑鼠", actual.Product_Name); + } + + [TestMethod] + public void 操作真實資料庫_注入EmployeeDbContext() + { + //arrange + DefaultDbContextManager.Now = new DateTime(1900, 1, 1); + + var connectionString = + "Server=(localdb)\\mssqllocaldb;Database=Lab.DAL.Injection;Trusted_Connection=True;MultipleActiveResultSets=true"; + + var builder = Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddDbContext(builder => + { + builder.UseSqlServer(connectionString); + }); + }); + var host = builder.Build(); + + var dbContextOptions = host.Services.GetService>(); + var repository = host.Services.GetService(); + var employeeDbContext = host.Services.GetService(); + employeeDbContext.Database.EnsureCreated(); + + repository.EmployeeDbContext = employeeDbContext; + + //act + var count = repository.NewAsync(new NewRequest + { + Account = "yao", + Password = "123456", + Name = "余小章", + Age = 18, + }, "TestUser").Result; + + //assert + Assert.AreEqual(2, count); + using var db = new EmployeeDbContext(dbContextOptions); + + var actual = db.Employees + .Include(p => p.Identity) + .AsNoTracking() + .FirstOrDefault(); + + Assert.AreEqual(actual.Name, "余小章"); + Assert.AreEqual(actual.Age, 18); + Assert.AreEqual(actual.Identity.Account, "yao"); + Assert.AreEqual(actual.Identity.Password, "123456"); + db.Database.EnsureDeleted(); + } + + [TestMethod] + public void 操作真實資料庫_預設EmployeeDbContext() + { + //arrange + DefaultDbContextManager.Now = new DateTime(1900, 1, 1); + DefaultDbContextManager.SetPhysicalDatabase(); + + var builder = Host.CreateDefaultBuilder() + .ConfigureServices(services => { services.AddSingleton(); }); + var host = builder.Build(); + + var repository = host.Services.GetService(); + + //act + var count = repository.NewAsync(new NewRequest + { + Account = "yao", + Password = "123456", + Name = "余小章", + Age = 18, + }, "TestUser").Result; + + //assert + Assert.AreEqual(2, count); + + using var db = new TestEmployeeDbContext(TestDbConnectionString); + + var actual = db.Employees + .Include(p => p.Identity) + .AsNoTracking() + .FirstOrDefault(); + + Assert.AreEqual(actual.Name, "余小章"); + Assert.AreEqual(actual.Age, 18); + Assert.AreEqual(actual.Identity.Account, "yao"); + Assert.AreEqual(actual.Identity.Password, "123456"); + } + + [TestMethod] + public void 操作記憶體() + { + DefaultDbContextManager.Now = new DateTime(1900, 1, 1); + DefaultDbContextManager.SetMemoryDatabase(); + + var builder = Host.CreateDefaultBuilder() + .ConfigureServices(services => { services.AddSingleton(); }); + var host = builder.Build(); + + var repository = host.Services.GetService(); + var count = repository.NewAsync(new NewRequest(), "TestUser").Result; + Assert.AreEqual(2, count); + } + + private static DbContextOptions CreateDbContextOptions() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + + var configRoot = configBuilder.Build(); + var connectionString = configRoot.GetConnectionString("DefaultConnection"); + + var loggerFactory = LoggerFactory.Create(builder => + { + builder + + //.AddFilter("Microsoft", LogLevel.Warning) + //.AddFilter("System", LogLevel.Warning) + .AddFilter("Lab.DAL", LogLevel.Debug) + .AddConsole() + ; + }); + return new DbContextOptionsBuilder() + .UseSqlServer(connectionString) + .UseLoggerFactory(loggerFactory) + .Options; + } + + private static void DeleteTestDataRow() + { + using var db = new TestEmployeeDbContext(TestDbConnectionString); + if (db.Database.CanConnect() == false) + { + return; + } + + var deleteCommand = GetDeleteAllRecordCommand(); + db.Database.ExecuteSqlRaw(deleteCommand); + } + + private static string GetDeleteAllRecordCommand() + { + var sql = @" +-- disable referential integrity +EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL' + + +EXEC sp_MSForEachTable 'DELETE FROM ?' + + +-- enable referential integrity again +EXEC sp_MSForEachTable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL' +"; + + return sql; + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/Lab.DAL.TestProject.csproj b/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/Lab.DAL.TestProject.csproj index 7b4935b5..4ac56c73 100644 --- a/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/Lab.DAL.TestProject.csproj +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/Lab.DAL.TestProject.csproj @@ -8,10 +8,15 @@ + + + + + diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/TestEmployeeDbContext.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/TestEmployeeDbContext.cs new file mode 100644 index 00000000..202ad138 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/TestEmployeeDbContext.cs @@ -0,0 +1,58 @@ +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; + +namespace Lab.DAL.TestProject +{ + public class TestEmployeeDbContext : DbContext + { + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identities { get; set; } + + public virtual DbSet OrderHistories { get; set; } + + private readonly string _connectionString; + + public TestEmployeeDbContext(string connectionString) + { + this._connectionString = connectionString; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + var connectionString = this._connectionString; + if (optionsBuilder.IsConfigured == false) + { + optionsBuilder.UseSqlServer(connectionString); + } + } + + //管理索引 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(p => + { + p.HasKey(e => e.Id) + .IsClustered(false); + }); + + modelBuilder.Entity(p => + { + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + }); + modelBuilder.Entity(p => + { + p.HasKey(e => e.Employee_Id) + .IsClustered(false); + }); + modelBuilder.Entity(p => + { + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + }); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/UnitTest1.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/UnitTest1.cs deleted file mode 100644 index 21530ddd..00000000 --- a/ORM/EFCore/Lab.VirtualDb/Lab.DAL.TestProject/UnitTest1.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Linq; -using Lab.DAL.EntityModel; -using Microsoft.EntityFrameworkCore; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Lab.DAL.UnitTest -{ - [TestClass] - public class UnitTest1 - { - [TestMethod] - public void TestMethod1() - { - var options = DbContextOptionManager.CreateEmployeeDbContextOptions(); - using (var dbContext = new EmployeeContext(options)) - { - var employees = dbContext.Employees.AsNoTracking().ToList(); - } - } - - [TestMethod] - public void TestMethod2() - { - var options = DbContextOptionManager.CreateEmployeeDbContextOptions(); - - using (var dbContext = new EmployeeContext(options)) - { - var id = Guid.NewGuid(); - var toDb = new Employee - { - Id = id, - Name = "yao", - Age = 18, - }; - dbContext.Employees.Add(toDb); - var count = dbContext.SaveChanges(); - Assert.AreEqual(true, count != 0); - Assert.AreEqual(true, toDb.SequenceId != 0); - } - } - } -} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DbContextOptionManager.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DbContextOptionManager.cs deleted file mode 100644 index 851b770f..00000000 --- a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DbContextOptionManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Lab.DAL.EntityModel; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Lab.DAL -{ - public class DbContextOptionManager - { - public static DbContextOptions CreateEmployeeDbContextOptions() - { - var configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .Build(); - var connectionString = configuration.GetConnectionString("DefaultConnection"); - var loggerFactory = LoggerFactory.Create(builder => - { - builder - //.AddFilter("Microsoft", LogLevel.Warning) - //.AddFilter("System", LogLevel.Warning) - .AddFilter("Lab.DAL", LogLevel.Debug) - .AddConsole() - ; - }); - return new DbContextOptionsBuilder() - .UseSqlServer(connectionString) - .UseLoggerFactory(loggerFactory) - .Options; - } - } -} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DbOptionsFactory.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DbOptionsFactory.cs deleted file mode 100644 index 059eec32..00000000 --- a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DbOptionsFactory.cs +++ /dev/null @@ -1,37 +0,0 @@ -// using Lab.DAL.EntityModel; -// using Microsoft.EntityFrameworkCore; -// using Microsoft.Extensions.Configuration; -// using Microsoft.Extensions.Logging; -// -// namespace Lab.DAL -// { -// public class DbOptionsFactory -// { -// public static DbContextOptions DbContextOptions { get; } -// -// public static string ConnectionString { get; } -// public static readonly ILoggerFactory MyLoggerFactory -// = LoggerFactory.Create(builder => { builder.AddConsole(); }); -// -// static DbOptionsFactory() -// { -// var configuration = new ConfigurationBuilder() -// .AddJsonFile("appsettings.json") -// .Build(); -// ConnectionString = configuration.GetConnectionString("DefaultConnection"); -// var loggerFactory = LoggerFactory.Create(builder => -// { -// builder -// //.AddFilter("Microsoft", LogLevel.Warning) -// //.AddFilter("System", LogLevel.Warning) -// .AddFilter("Lab.DAL", LogLevel.Debug) -// .AddConsole() -// ; -// }); -// DbContextOptions = new DbContextOptionsBuilder() -// .UseSqlServer(ConnectionString) -// .UseLoggerFactory(loggerFactory) -// .Options; -// } -// } -// } \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DefaultDbContextManager.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DefaultDbContextManager.cs new file mode 100644 index 00000000..999d62af --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DefaultDbContextManager.cs @@ -0,0 +1,141 @@ +using System; +using System.IO; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.DAL +{ + internal class DefaultDbContextManager + { + private static readonly Lazy s_serviceProviderLazy; + private static readonly Lazy s_configurationLazy; + private static readonly ILoggerFactory s_loggerFactory; + + private static readonly ServiceCollection s_services; + private static ServiceProvider s_serviceProvider; + private static IConfiguration s_configuration; + private static DateTime? s_now; + + public static DateTime Now + { + get + { + if (s_now == null) + { + return DateTime.UtcNow; + } + + return s_now.Value; + } + set => s_now = value; + } + + public static ServiceProvider ServiceProvider + { + get + { + if (s_serviceProvider == null) + { + s_serviceProvider = s_serviceProviderLazy.Value; + } + + return s_serviceProvider; + } + set => s_serviceProvider = value; + } + + public static IConfiguration Configuration + { + get + { + if (s_configuration == null) + { + s_configuration = s_configurationLazy.Value; + } + + return s_configuration; + } + set => s_configuration = value; + } + + static DefaultDbContextManager() + { + s_services = new ServiceCollection(); + + s_serviceProviderLazy = + new Lazy(() => + { + var services = s_services; + services.AddDbContextFactory(ApplyConfigurePhysical); + return services.BuildServiceProvider(); + }); + s_configurationLazy + = new Lazy(() => + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + return configBuilder.Build(); + }); + s_loggerFactory = LoggerFactory.Create(builder => + { + builder + + //.AddFilter("Microsoft", LogLevel.Warning) + //.AddFilter("System", LogLevel.Warning) + .AddFilter("Lab.DAL", LogLevel.Debug) + .AddConsole() + ; + }); + } + + public static T GetInstance() + { + return ServiceProvider.GetService(); + } + + public static void SetMemoryDatabase() where TContext : DbContext + { + var services = s_services; + + services.Clear(); + services.AddDbContextFactory(ApplyConfigureMemory); + ServiceProvider = services.BuildServiceProvider(); + } + + public static void SetPhysicalDatabase() where TContext : DbContext + { + var services = s_services; + + services.Clear(); + services.AddDbContextFactory(ApplyConfigurePhysical); + ServiceProvider = services.BuildServiceProvider(); + } + + private static void ApplyConfigureMemory(IServiceProvider provider, + DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseInMemoryDatabase("Lab.DAL") + .UseLoggerFactory(s_loggerFactory) + ; + } + + private static void ApplyConfigurePhysical(IServiceProvider provider, + DbContextOptionsBuilder optionsBuilder) + { + var config = provider.GetService(); + if (config == null) + { + config = Configuration; + } + + var connectionString = config.GetConnectionString("DefaultConnection"); + optionsBuilder.UseSqlServer(connectionString) + .UseLoggerFactory(s_loggerFactory) + ; + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DomainModel/Employee/FilterResponse.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DomainModel/Employee/FilterResponse.cs new file mode 100644 index 00000000..8aa958a4 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DomainModel/Employee/FilterResponse.cs @@ -0,0 +1,15 @@ +namespace Lab.DAL.DomainModel.Employee +{ + public class FilterResponse + { + public string Name { get; set; } + + public int? Age { get; set; } + + public string Account { get; set; } + + public string Password { get; set; } + + public string Remark { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DomainModel/Employee/InsertOrderRequest.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DomainModel/Employee/InsertOrderRequest.cs new file mode 100644 index 00000000..76eb01d6 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DomainModel/Employee/InsertOrderRequest.cs @@ -0,0 +1,15 @@ +using System; + +namespace Lab.DAL.DomainModel.Employee +{ + public class InsertOrderRequest + { + public Guid? Employee_Id { get; set; } + + public string Product_Id { get; set; } + + public string Product_Name { get; set; } + + public string Remark { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DomainModel/Employee/NewRequest.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DomainModel/Employee/NewRequest.cs new file mode 100644 index 00000000..ed8418dc --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/DomainModel/Employee/NewRequest.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Lab.DAL.DomainModel.Employee +{ + public class NewRequest + { + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + public string Remark { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EmployeeContextFactory.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EmployeeContextFactory.cs index 3214e39e..58195dd9 100644 --- a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EmployeeContextFactory.cs +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EmployeeContextFactory.cs @@ -1,19 +1,29 @@ -using Lab.DAL.EntityModel; +using System; +using System.IO; +using Lab.DAL.EntityModel; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; namespace Lab.DAL { - public class EmployeeContextFactory + public class EmployeeContextFactory : IDesignTimeDbContextFactory { - private DbContextOptions _options; - - public EmployeeContextFactory(DbContextOptions options) + public EmployeeDbContext CreateDbContext(string[] args) { - this._options = options; - } + Console.WriteLine("由設計工具產生 Database,初始化 DbContextOptionsBuilder"); - public EmployeeContextFactory():this(DbContextOptionManager.CreateEmployeeDbContextOptions()) - { + var config = DefaultDbContextManager.Configuration; + var connectionString = config.GetConnectionString("DefaultConnection"); + + Console.WriteLine($"由 appsettings.json 讀取連線字串為:{connectionString}"); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer(connectionString); + Console.WriteLine($"DbContextOptionsBuilder 設定完成"); + + return new EmployeeDbContext(optionsBuilder.Options); } } } \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EmployeeRepository.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EmployeeRepository.cs new file mode 100644 index 00000000..68c05d52 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EmployeeRepository.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Lab.DAL.DomainModel.Employee; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; + +namespace Lab.DAL +{ + public interface IEmployeeRepository + { + Task InsertLogAsync(InsertOrderRequest request, + string accessId, + CancellationToken cancel = default); + + Task NewAsync(NewRequest request, + string accessId, + CancellationToken cancel = default); + } + + public class EmployeeRepository : IEmployeeRepository + { + internal IDbContextFactory DbContextFactory + { + get + { + if (this._dbContextFactory == null) + { + return DefaultDbContextManager.GetInstance>(); + } + + return this._dbContextFactory; + } + set => this._dbContextFactory = value; + } + + internal EmployeeDbContext EmployeeDbContext + { + get + { + if (this._employeeDbContext == null) + { + return this.DbContextFactory.CreateDbContext(); + } + + return this._employeeDbContext; + } + set => this._employeeDbContext = value; + } + + internal DateTime Now + { + get + { + if (this._now == null) + { + return DefaultDbContextManager.Now; + } + + return this._now.Value; + } + set => this._now = value; + } + + private IDbContextFactory _dbContextFactory; + private EmployeeDbContext _employeeDbContext; + private DateTime? _now; + + public async Task InsertLogAsync(InsertOrderRequest request, + string accessId, + CancellationToken cancel = default) + { + await using var dbContext = this.EmployeeDbContext; + + var toDbOrderHistory = new OrderHistory + { + Employee_Id = request.Employee_Id, + Product_Id = request.Product_Id, + Product_Name = request.Product_Name, + CreateAt = this.Now, + CreateBy = accessId, + Remark = request.Remark, + }; + + await dbContext.OrderHistories.AddAsync(toDbOrderHistory, cancel); + return await dbContext.SaveChangesAsync(cancel); + } + + public async Task NewAsync(NewRequest request, + string accessId, + CancellationToken cancel = default) + { + await using var dbContext = this.EmployeeDbContext; + + var id = Guid.NewGuid(); + var employeeToDb = new Employee + { + Id = id, + Name = request.Name, + Age = request.Age, + Remark = request.Remark, + CreateAt = this.Now, + CreateBy = accessId + }; + + var identityToDb = new Identity + { + Account = request.Account, + Password = request.Password, + Remark = request.Remark, + Employee = employeeToDb, + CreateAt = this.Now, + CreateBy = accessId + }; + + employeeToDb.Identity = identityToDb; + await dbContext.Employees.AddAsync(employeeToDb, cancel); + return await dbContext.SaveChangesAsync(cancel); + } + + public async Task> GetAllAsync(CancellationToken cancel) + { + await using var db = this.EmployeeDbContext; + return await db.Employees + .Include(p => p.Identity) + .Select(p => new FilterResponse() + { + Account = p.Identity.Account, + Age = p.Age, + Name = p.Name, + Password = p.Identity.Password, + Remark = p.Remark + }) + .AsNoTracking() + .ToListAsync(cancel); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/Employee.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/Employee.cs index 678508f4..2c375566 100644 --- a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/Employee.cs +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/Employee.cs @@ -1,18 +1,17 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Lab.DAL.EntityModel { [Table("Employee")] - public class Employee { [Key] [DatabaseGenerated(DatabaseGeneratedOption.None)] public Guid Id { get; set; } + [Required] public string Name { get; set; } public int? Age { get; set; } @@ -22,13 +21,12 @@ public class Employee public string Remark { get; set; } - public virtual Identity Identity { get; set; } + [Required] + public DateTime CreateAt { get; set; } - public virtual ICollection Order { get; set; } + [Required] + public string CreateBy { get; set; } - public Employee() - { - this.Order = new HashSet(); - } + public virtual Identity Identity { get; set; } } } \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/EmployeeContext.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/EmployeeContext.cs deleted file mode 100644 index 142d3f56..00000000 --- a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/EmployeeContext.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Lab.DAL.EntityModel -{ - public class EmployeeContext : DbContext - { - private static readonly bool[] s_migrated = {false}; - - public virtual DbSet Employees { get; set; } - - public virtual DbSet Identities { get; set; } - - public virtual DbSet Orders { get; set; } - - public EmployeeContext() - { - - } - - public EmployeeContext(DbContextOptions options) - : base(options) - { - if (!s_migrated[0]) - { - lock (s_migrated) - { - if (!s_migrated[0]) - { - this.Database.Migrate(); - s_migrated[0] = true; - } - } - } - } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { -// //var connectionString = "Server=(localdb)\\mssqllocaldb;Database=LabEmployee.DAL;Trusted_Connection=True;"; -// var connectionString = DbOptionsFactory.ConnectionString; -// if (!optionsBuilder.IsConfigured) -// { -// #warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings. -// optionsBuilder -// .UseSqlServer(connectionString); -// } - } - } -} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/EmployeeDbContext.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..78b0f738 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,89 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.InMemory.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; + +namespace Lab.DAL.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = {false}; + + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identities { get; set; } + + public virtual DbSet OrderHistories { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + if (s_migrated[0]) + { + return; + } + + lock (s_migrated) + { + if (s_migrated[0] == false) + { + var memoryOptions = options.FindExtension(); + + if (memoryOptions == null) + { + var sqlOptions = options.FindExtension(); + if (sqlOptions != null) + { + Console.WriteLine($"EmployeeDbContext of connection string be '{sqlOptions.ConnectionString}'"); + this.Database.Migrate(); + } + } + + s_migrated[0] = true; + } + } + } + + // 給 Migration CLI 使用 + // 建構函數配置失敗才需要以下處理 + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // var connectionString = + // "Server=(localdb)\\mssqllocaldb;Database=Lab.DAL.UnitTest;Trusted_Connection=True;MultipleActiveResultSets=true"; + // + // // var connectionString = this._connectionString; + // if (optionsBuilder.IsConfigured == false) + // { + // optionsBuilder.UseSqlServer(connectionString); + // } + } + + //管理索引 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(p => + { + p.HasKey(e => e.Id) + .IsClustered(false); + }); + + modelBuilder.Entity(p => + { + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + }); + modelBuilder.Entity(p => + { + p.HasKey(e => e.Employee_Id) + .IsClustered(false); + }); + modelBuilder.Entity(p => + { + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + }); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/Identity.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/Identity.cs index a59ffab2..472f9bcb 100644 --- a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/Identity.cs +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/Identity.cs @@ -9,16 +9,26 @@ public class Identity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.None)] - public Guid EmployeeId { get; set; } + public Guid Employee_Id { get; set; } + [Required] public string Account { get; set; } + [Required] public string Password { get; set; } + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long SequenceId { get; set; } public string Remark { get; set; } + [Required] + public DateTime CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + [ForeignKey("Employee_Id")] public virtual Employee Employee { get; set; } } } \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/Order.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/Order.cs deleted file mode 100644 index aca86633..00000000 --- a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/Order.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Lab.DAL.EntityModel -{ - [Table("Order")] - public class Order - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public Guid Id { get; set; } - - public Guid? EmployeeId { get; set; } - - public DateTime? OrderTime { get; set; } - - public string Remark { get; set; } - - public long SequenceId { get; set; } - - public virtual Employee Employee { get; set; } - } -} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/OrderHistory.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/OrderHistory.cs new file mode 100644 index 00000000..ca49d134 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/EntityModel/OrderHistory.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.DAL.EntityModel +{ + [Table("OrderHistory")] + public class OrderHistory + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public Guid? Employee_Id { get; set; } + + public string Remark { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Product_Id { get; set; } + + public string Product_Name { get; set; } + + [Required] + public DateTime CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Lab.DAL.csproj b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Lab.DAL.csproj index 99c5ead8..77c9a56d 100644 --- a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Lab.DAL.csproj +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Lab.DAL.csproj @@ -6,16 +6,20 @@ bin\Lab.DAL.xml - - + + + - Always - + + + <_Parameter1>Lab.DAL.TestProject + + diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Migrations/20210415015614_InitialCreate.Designer.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Migrations/20210415015614_InitialCreate.Designer.cs new file mode 100644 index 00000000..da6dc416 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Migrations/20210415015614_InitialCreate.Designer.cs @@ -0,0 +1,152 @@ +// +using System; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Lab.DAL.Migrations +{ + [DbContext(typeof(EmployeeDbContext))] + [Migration("20210415015614_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseIdentityColumns() + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.0"); + + modelBuilder.Entity("Lab.DAL.EntityModel.Employee", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Age") + .HasColumnType("int"); + + b.Property("CreateAt") + .HasColumnType("datetime2"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Remark") + .HasColumnType("nvarchar(max)"); + + b.Property("SequenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.HasKey("Id") + .IsClustered(false); + + b.HasIndex("SequenceId") + .IsUnique() + .IsClustered(); + + b.ToTable("Employee"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Identity", b => + { + b.Property("Employee_Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Account") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreateAt") + .HasColumnType("datetime2"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Remark") + .HasColumnType("nvarchar(max)"); + + b.Property("SequenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.HasKey("Employee_Id") + .IsClustered(false); + + b.HasIndex("SequenceId") + .IsUnique() + .IsClustered(); + + b.ToTable("Identity"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.OrderHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreateAt") + .HasColumnType("datetime2"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Employee_Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Product_Id") + .HasColumnType("nvarchar(max)"); + + b.Property("Product_Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Remark") + .HasColumnType("nvarchar(max)"); + + b.Property("SequenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.HasKey("Id"); + + b.ToTable("OrderHistory"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Identity", b => + { + b.HasOne("Lab.DAL.EntityModel.Employee", "Employee") + .WithOne("Identity") + .HasForeignKey("Lab.DAL.EntityModel.Identity", "Employee_Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Employee", b => + { + b.Navigation("Identity"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Migrations/20210415015614_InitialCreate.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Migrations/20210415015614_InitialCreate.cs new file mode 100644 index 00000000..44edf0c5 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Migrations/20210415015614_InitialCreate.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Lab.DAL.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Employee", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Age = table.Column(type: "int", nullable: true), + SequenceId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Remark = table.Column(type: "nvarchar(max)", nullable: true), + CreateAt = table.Column(type: "datetime2", nullable: false), + CreateBy = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Employee", x => x.Id) + .Annotation("SqlServer:Clustered", false); + }); + + migrationBuilder.CreateTable( + name: "OrderHistory", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Employee_Id = table.Column(type: "uniqueidentifier", nullable: true), + Remark = table.Column(type: "nvarchar(max)", nullable: true), + SequenceId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Product_Id = table.Column(type: "nvarchar(max)", nullable: true), + Product_Name = table.Column(type: "nvarchar(max)", nullable: true), + CreateAt = table.Column(type: "datetime2", nullable: false), + CreateBy = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderHistory", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Identity", + columns: table => new + { + Employee_Id = table.Column(type: "uniqueidentifier", nullable: false), + Account = table.Column(type: "nvarchar(max)", nullable: false), + Password = table.Column(type: "nvarchar(max)", nullable: false), + SequenceId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Remark = table.Column(type: "nvarchar(max)", nullable: true), + CreateAt = table.Column(type: "datetime2", nullable: false), + CreateBy = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Identity", x => x.Employee_Id) + .Annotation("SqlServer:Clustered", false); + table.ForeignKey( + name: "FK_Identity_Employee_Employee_Id", + column: x => x.Employee_Id, + principalTable: "Employee", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Employee_SequenceId", + table: "Employee", + column: "SequenceId", + unique: true) + .Annotation("SqlServer:Clustered", true); + + migrationBuilder.CreateIndex( + name: "IX_Identity_SequenceId", + table: "Identity", + column: "SequenceId", + unique: true) + .Annotation("SqlServer:Clustered", true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Identity"); + + migrationBuilder.DropTable( + name: "OrderHistory"); + + migrationBuilder.DropTable( + name: "Employee"); + } + } +} diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Migrations/EmployeeDbContextModelSnapshot.cs b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Migrations/EmployeeDbContextModelSnapshot.cs new file mode 100644 index 00000000..d9b6dd49 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.DAL/Migrations/EmployeeDbContextModelSnapshot.cs @@ -0,0 +1,150 @@ +// +using System; +using Lab.DAL.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Lab.DAL.Migrations +{ + [DbContext(typeof(EmployeeDbContext))] + partial class EmployeeDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseIdentityColumns() + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.0"); + + modelBuilder.Entity("Lab.DAL.EntityModel.Employee", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Age") + .HasColumnType("int"); + + b.Property("CreateAt") + .HasColumnType("datetime2"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Remark") + .HasColumnType("nvarchar(max)"); + + b.Property("SequenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.HasKey("Id") + .IsClustered(false); + + b.HasIndex("SequenceId") + .IsUnique() + .IsClustered(); + + b.ToTable("Employee"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Identity", b => + { + b.Property("Employee_Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Account") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreateAt") + .HasColumnType("datetime2"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Remark") + .HasColumnType("nvarchar(max)"); + + b.Property("SequenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.HasKey("Employee_Id") + .IsClustered(false); + + b.HasIndex("SequenceId") + .IsUnique() + .IsClustered(); + + b.ToTable("Identity"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.OrderHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreateAt") + .HasColumnType("datetime2"); + + b.Property("CreateBy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Employee_Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Product_Id") + .HasColumnType("nvarchar(max)"); + + b.Property("Product_Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Remark") + .HasColumnType("nvarchar(max)"); + + b.Property("SequenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.HasKey("Id"); + + b.ToTable("OrderHistory"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Identity", b => + { + b.HasOne("Lab.DAL.EntityModel.Employee", "Employee") + .WithOne("Identity") + .HasForeignKey("Lab.DAL.EntityModel.Identity", "Employee_Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Lab.DAL.EntityModel.Employee", b => + { + b.Navigation("Identity"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.VirtualDb.sln b/ORM/EFCore/Lab.VirtualDb/Lab.VirtualDb.sln index 2d4c828a..fa88dca5 100644 --- a/ORM/EFCore/Lab.VirtualDb/Lab.VirtualDb.sln +++ b/ORM/EFCore/Lab.VirtualDb/Lab.VirtualDb.sln @@ -4,6 +4,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.DAL", "Lab.DAL\Lab.DAL. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.DAL.TestProject", "Lab.DAL.TestProject\Lab.DAL.TestProject.csproj", "{3DAC8D3D-E494-459B-BEAD-AD306034E441}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Biz", "Lab.Biz\Lab.Biz.csproj", "{0056BEF7-8B47-4387-9110-788A3B73E452}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.WebApi", "Lab.WebApi\Lab.WebApi.csproj", "{7C5880FB-4D25-4188-A04C-CE58C6D592A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +22,13 @@ Global {3DAC8D3D-E494-459B-BEAD-AD306034E441}.Debug|Any CPU.Build.0 = Debug|Any CPU {3DAC8D3D-E494-459B-BEAD-AD306034E441}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DAC8D3D-E494-459B-BEAD-AD306034E441}.Release|Any CPU.Build.0 = Release|Any CPU + {0056BEF7-8B47-4387-9110-788A3B73E452}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0056BEF7-8B47-4387-9110-788A3B73E452}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0056BEF7-8B47-4387-9110-788A3B73E452}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0056BEF7-8B47-4387-9110-788A3B73E452}.Release|Any CPU.Build.0 = Release|Any CPU + {7C5880FB-4D25-4188-A04C-CE58C6D592A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C5880FB-4D25-4188-A04C-CE58C6D592A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C5880FB-4D25-4188-A04C-CE58C6D592A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C5880FB-4D25-4188-A04C-CE58C6D592A0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Controllers/DefaultController.cs b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Controllers/DefaultController.cs new file mode 100644 index 00000000..385cb904 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Controllers/DefaultController.cs @@ -0,0 +1,57 @@ +using System.Threading; +using System.Threading.Tasks; +using Lab.DAL; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using ServiceModel = Lab.WebApi.ServiceModel; +using DomainModel = Lab.DAL.DomainModel; + +namespace Lab.WebApi.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DefaultController : ControllerBase + { + private readonly ILogger _logger; + private readonly EmployeeRepository _repository; + + public DefaultController(ILogger logger, EmployeeRepository repository) + { + this._logger = logger; + this._repository = repository; + } + + [HttpGet] + [Produces(typeof(ServiceModel.Employee.FilterResponse))] + public async Task Get(CancellationToken cancel = default) + { + var repository = this._repository; + var record = await repository.GetAllAsync(cancel); + return this.Ok(record); + } + + [HttpPost] + public async Task Post(ServiceModel.Employee.NewRequest request, + string accessId, + CancellationToken cancel = default) + { + var repository = this._repository; + var count = await repository.NewAsync(new DomainModel.Employee.NewRequest() + { + Account = request.Account, + Age = request.Age, + Name = request.Name, + Password = request.Password, + Remark = request.Remark + }, accessId, cancel); + if (count == 2) + { + return this.Ok(); + } + else + { + return this.NoContent(); + } + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Lab.WebApi.csproj b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Lab.WebApi.csproj new file mode 100644 index 00000000..8b969acc --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Lab.WebApi.csproj @@ -0,0 +1,15 @@ + + + + net5.0 + + + + + + + + + + + diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Program.cs b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Program.cs new file mode 100644 index 00000000..3fd195ba --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Program.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Lab.WebApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Properties/launchSettings.json b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Properties/launchSettings.json new file mode 100644 index 00000000..292d93d5 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:24269", + "sslPort": 44335 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Lab.WebApi": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/ServiceModel/Employee/FilterResponse.cs b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/ServiceModel/Employee/FilterResponse.cs new file mode 100644 index 00000000..257ab643 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/ServiceModel/Employee/FilterResponse.cs @@ -0,0 +1,16 @@ +namespace Lab.WebApi.ServiceModel.Employee +{ + //for doc + public class FilterResponse + { + public string Name { get; set; } + + public int? Age { get; set; } + + public string Account { get; set; } + + public string Password { get; set; } + + public string Remark { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/ServiceModel/Employee/NewRequest.cs b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/ServiceModel/Employee/NewRequest.cs new file mode 100644 index 00000000..e26362fb --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/ServiceModel/Employee/NewRequest.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace Lab.WebApi.ServiceModel.Employee +{ + public class NewRequest + { + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + public string Remark { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Startup.cs b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Startup.cs new file mode 100644 index 00000000..4430ede3 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/Startup.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Lab.DAL; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; + +namespace Lab.WebApi +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo {Title = "Lab.WebApi", Version = "v1"}); + }); + services.AddSingleton(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Lab.WebApi v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/WeatherForecast.cs b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/WeatherForecast.cs new file mode 100644 index 00000000..1c8dfc31 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/WeatherForecast.cs @@ -0,0 +1,15 @@ +using System; + +namespace Lab.WebApi +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int) (TemperatureC / 0.5556); + + public string Summary { get; set; } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/appsettings.Development.json b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/appsettings.json b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/appsettings.json new file mode 100644 index 00000000..64195df2 --- /dev/null +++ b/ORM/EFCore/Lab.VirtualDb/Lab.WebApi/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=Lab.DAL.WebApi;Trusted_Connection=True;MultipleActiveResultSets=true" + } +} diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/EfCoreRawSqlUnitTest.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/EfCoreRawSqlUnitTest.cs new file mode 100644 index 00000000..b4e89ee2 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/EfCoreRawSqlUnitTest.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using EFCore.BulkExtensions; +using Faker; +using Lab.ORM.DynamicField.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.ORM.DynamicField.UnitTest; + +[TestClass] +public class EFCoreRawSqlUnitTest +{ + [TestCleanup] + public void TestCleanup() + { + CleanData(); + } + + [TestInitialize] + public void TestInitialize() + { + CleanData(); + } + + [TestMethod] + public void TestMethod2() + { + var host = CreateHostBuilder(null).Start(); + host.Services.GetService>(); + } + + + [TestMethod] + public void 查詢所有資料() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var newEmployee = Insert(); + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + + var actual = db.Employees.FromSqlRaw(@" +SELECT * +FROM ""Employee"" AS e +LIMIT 1 +").ToList(); + } + + [TestMethod] + public void 查詢特定欄位_JsonDoc() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var newEmployee = Insert(); + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + var actual = db.Employees + + // .Where(p => p.Profiles.RootElement.GetProperty("long").GetString() == "255") + .Where(p => p.Profiles.RootElement.GetProperty("long").GetInt64() == 255) + .Select(p => new + { + Profiles = p.Profiles.To>(options), + }) + .FirstOrDefault(); + } + + [TestMethod] + public void 查詢特定欄位_POCO() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var newEmployee = Insert(); + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + var actual = db.Employees + + // .Where(p => p.Customer.Age > 12) + .Select(p => new + { + p.Customer + + // Order = p.Customer.Orders.Select(p => new { p.Price, p.ShippingAddress }) + // Order = p.Customer + // .Orders + // .Select(p => new Order + // { + // Price = p.Price + // }) + // + + // aa = p.Customer.Orders.ToDictionary(p => p.Price, p => p.ShippingAddress) + }) + + // .AsAsyncEnumerable() + .FirstOrDefault() + ; + } + + [TestMethod] + public void 新增資料() + { + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + var expected = new Dictionary + { + ["anonymousType"] = new { Prop = 123 }, + ["model"] = new Model { Age = 19, Name = "yao" }, + ["null"] = null!, + ["dateTimeOffset"] = DateTimeOffset.Now, + ["long"] = (long)255, + ["decimal"] = (decimal)3.1416, + ["guid"] = Guid.NewGuid(), + ["string"] = "String", + ["decimalArray"] = new[] { 1, (decimal)2.1 }, + }; + + var id = Guid.NewGuid(); + db.Employees.Add(new Employee + { + Id = id, + Age = RandomNumber.Next(1, 100), + Name = Name.FullName(), + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "Sys", + Profiles = expected.ToJsonDocument(), + }); + + db.SaveChanges(); + } + + private static void CleanData() + { + using var dbContext = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + dbContext.Employees.BatchDelete(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureServices((hostBuilder, services) => { TestAssistant.ConfigureTestServices(services); }); + } + + private static List GenerateEmployees(int totalCount) + { + var employees = Enumerable.Range(0, totalCount) + .Select((x, i) => + { + var now = DateTimeOffset.UtcNow; + var sysAccount = "sys"; + return new Employee + { + Id = Guid.NewGuid(), + Age = RandomNumber.Next(1, 100), + Name = Name.FullName(), + CreatedBy = sysAccount, + CreatedAt = now, + ModifiedAt = null, + ModifiedBy = null, + + // Name = Name.First(), + }; + }).ToList(); + return employees; + } + + private static Employee Insert() + { + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + var expected = new Dictionary + { + ["anonymousType"] = new { Prop = 123 }, + ["model"] = new Model { Age = 19, Name = "yao" }, + ["null"] = null!, + ["dateTimeOffset"] = DateTimeOffset.Now, + ["long"] = (long)255, + ["decimal"] = (decimal)3.1416, + ["guid"] = Guid.NewGuid(), + ["string"] = "String", + ["decimalArray"] = new[] { 1, (decimal)2.1 }, + }; + + var id = Guid.NewGuid(); + var newEmployee = new Employee + { + Id = id, + Age = RandomNumber.Next(1, 100), + Name = Name.FullName(), + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "Sys", + Profiles = expected.ToJsonDocument(), + Customer = new Customer + { + Age = 19, + Name = "小章", + Orders = new[] + { + new Order + { + Price = (decimal)22.1, + ShippingAddress = "台北市" + } + }, + Product = new Product + { + Id = Guid.NewGuid(), + Name = "Mouse" + } + } + }; + db.Employees.Add(newEmployee); + + db.SaveChanges(); + return newEmployee; + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/EfCoreUnitTest.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/EfCoreUnitTest.cs new file mode 100644 index 00000000..7b0b89a3 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/EfCoreUnitTest.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using EFCore.BulkExtensions; +using Faker; +using Lab.ORM.DynamicField.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.ORM.DynamicField.UnitTest; + +[TestClass] +public class EfCoreUnitTest +{ + [TestCleanup] + public void TestCleanup() + { + CleanData(); + } + + [TestInitialize] + public void TestInitialize() + { + CleanData(); + } + + [TestMethod] + public void TestMethod2() + { + var host = CreateHostBuilder(null).Start(); + host.Services.GetService>(); + } + + [TestMethod] + public void 更新部分欄位() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var newEmployee = Insert(); + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + var destEmployee = new Employee + { + Id = newEmployee.Id, + Name = "yao", + ModifiedAt = DateTimeOffset.UtcNow, + ModifiedBy = "sys" + }; + var employeeEntry = db.Entry(destEmployee); + db.Attach(destEmployee); + employeeEntry.Property(p => p.Name).IsModified = true; + employeeEntry.Property(p => p.ModifiedAt).IsModified = true; + employeeEntry.Property(p => p.ModifiedBy).IsModified = true; + var count = db.SaveChanges(); + } + + [TestMethod] + public void 查詢所有資料() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var newEmployee = Insert(); + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + var actual = db.Employees + .Where(p => p.Id == newEmployee.Id) + .Select(p => new + { + Profiles = p.Profiles.To>(options), + }) + .FirstOrDefault(); + } + + [TestMethod] + public void 查詢特定欄位_JsonDoc() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var newEmployee = Insert(); + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + var actual = db.Employees + + // .Where(p => p.Profiles.RootElement.GetProperty("long").GetString() == "255") + .Where(p => p.Profiles.RootElement.GetProperty("long").GetInt64() == 255) + .Select(p => new + { + Profiles = p.Profiles.To>(options), + }) + .FirstOrDefault(); + } + + [TestMethod] + public void 查詢特定欄位_POCO() + { + var options = new JsonSerializerOptions + { + Converters = { new DictionaryStringObjectJsonConverter() } + }; + var newEmployee = Insert(); + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + var actual = db.Employees + + // .Where(p => p.Customer.Age > 12) + .Select(p => new + { + p.Customer + + // Order = p.Customer.Orders.Select(p => new { p.Price, p.ShippingAddress }) + // Order = p.Customer + // .Orders + // .Select(p => new Order + // { + // Price = p.Price + // }) + // + + // aa = p.Customer.Orders.ToDictionary(p => p.Price, p => p.ShippingAddress) + }) + + // .AsAsyncEnumerable() + .FirstOrDefault() + ; + } + + [TestMethod] + public void 新增資料() + { + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + var expected = new Dictionary + { + ["anonymousType"] = new { Prop = 123 }, + ["model"] = new Model { Age = 19, Name = "yao" }, + ["null"] = null!, + ["dateTimeOffset"] = DateTimeOffset.Now, + ["long"] = (long)255, + ["decimal"] = (decimal)3.1416, + ["guid"] = Guid.NewGuid(), + ["string"] = "String", + ["decimalArray"] = new[] { 1, (decimal)2.1 }, + }; + + var id = Guid.NewGuid(); + db.Employees.Add(new Employee + { + Id = id, + Age = RandomNumber.Next(1, 100), + Name = Name.FullName(), + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "Sys", + Profiles = expected.ToJsonDocument(), + }); + + db.SaveChanges(); + } + + private static void CleanData() + { + using var dbContext = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + dbContext.Employees.BatchDelete(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureServices((hostBuilder, services) => { TestAssistant.ConfigureTestServices(services); }); + } + + private static List GenerateEmployees(int totalCount) + { + var employees = Enumerable.Range(0, totalCount) + .Select((x, i) => + { + var now = DateTimeOffset.UtcNow; + var sysAccount = "sys"; + return new Employee + { + Id = Guid.NewGuid(), + Age = RandomNumber.Next(1, 100), + Name = Name.FullName(), + CreatedBy = sysAccount, + CreatedAt = now, + ModifiedAt = null, + ModifiedBy = null, + + // Name = Name.First(), + }; + }).ToList(); + return employees; + } + + private static Employee Insert() + { + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + var expected = new Dictionary + { + ["anonymousType"] = new { Prop = 123 }, + ["model"] = new Model { Age = 19, Name = "yao" }, + ["null"] = null!, + ["dateTimeOffset"] = DateTimeOffset.Now, + ["long"] = (long)255, + ["decimal"] = (decimal)3.1416, + ["guid"] = Guid.NewGuid(), + ["string"] = "String", + ["decimalArray"] = new[] { 1, (decimal)2.1 }, + }; + + var id = Guid.NewGuid(); + var newEmployee = new Employee + { + Id = id, + Age = RandomNumber.Next(1, 100), + Name = Name.FullName(), + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "Sys", + Profiles = expected.ToJsonDocument(), + Customer = new Customer + { + Age = 19, + Name = "小章", + Orders = new[] + { + new Order + { + Price = (decimal)22.1, + ShippingAddress = "台北市" + } + }, + Product = new Product + { + Id = Guid.NewGuid(), + Name = "Mouse" + } + } + }; + db.Employees.Add(newEmployee); + + db.SaveChanges(); + return newEmployee; + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/GlobalSteps.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/GlobalSteps.cs new file mode 100644 index 00000000..5dc33201 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/GlobalSteps.cs @@ -0,0 +1,24 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.ORM.DynamicField.UnitTest; + +[TestClass] +public class GlobalSteps +{ + [AssemblyCleanup] + public static void Cleanup() + { + TestAssistant.SetTestEnvironmentVariable(); + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + db.Database.EnsureDeleted(); + } + + [AssemblyInitialize] + public static void Setup(TestContext context) + { + TestAssistant.SetTestEnvironmentVariable(); + using var db = TestAssistant.EmployeeDbContextFactory.CreateDbContext(); + db.Database.EnsureDeleted(); + db.Database.EnsureCreated(); + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/Lab.ORM.DynamicField.UnitTest.csproj b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/Lab.ORM.DynamicField.UnitTest.csproj new file mode 100644 index 00000000..9512e0fb --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/Lab.ORM.DynamicField.UnitTest.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/Model.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/Model.cs new file mode 100644 index 00000000..766e0dc7 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/Model.cs @@ -0,0 +1,8 @@ +namespace Lab.ORM.DynamicField.UnitTest; + +public record Model +{ + public int Age { get; set; } + + public string Name { get; set; } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/TestAssistant.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/TestAssistant.cs new file mode 100644 index 00000000..be4cb416 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.UnitTest/TestAssistant.cs @@ -0,0 +1,39 @@ +using System; +using Lab.ORM.DynamicField.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Lab.ORM.DynamicField.UnitTest; + +internal class TestAssistant +{ + private static IServiceProvider _serviceProvider; + + public static IDbContextFactory EmployeeDbContextFactory => + _serviceProvider.GetService>(); + + static TestAssistant() + { + var services = new ServiceCollection(); + ConfigureTestServices(services); + } + + public static void ConfigureTestServices(IServiceCollection services) + { + services.AddAppEnvironment(); + services.AddEntityFramework(); + _serviceProvider = services.BuildServiceProvider(); + } + + public static void SetTestEnvironmentVariable() + { + var connectionString = + "Host=localhost;Port=5432;Database=employee;Username=postgres;Password=guest;"; + + // var connectionString = + // "Data Source=localhost;Initial Catalog=EmployeeDb;Integrated Security=false;User ID=sa;Password=pass@w0rd1~;MultipleActiveResultSets=True;TrustServerCertificate=True"; + + var option = _serviceProvider.GetService(); + option.EmployeeDbConnectionString = connectionString; + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.sln b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.sln new file mode 100644 index 00000000..0ff4a8f9 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ORM.DynamicField", "Lab.ORM.DynamicField\Lab.ORM.DynamicField.csproj", "{C1348BD0-4FCA-4DF6-9C94-B2543F4B6E88}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ORM.DynamicField.UnitTest", "Lab.ORM.DynamicField.UnitTest\Lab.ORM.DynamicField.UnitTest.csproj", "{BC21E9D0-E865-499B-8395-4067E5E7DB09}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{B1431E24-8BEB-4481-9E42-8B66BBF99494}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C1348BD0-4FCA-4DF6-9C94-B2543F4B6E88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1348BD0-4FCA-4DF6-9C94-B2543F4B6E88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1348BD0-4FCA-4DF6-9C94-B2543F4B6E88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1348BD0-4FCA-4DF6-9C94-B2543F4B6E88}.Release|Any CPU.Build.0 = Release|Any CPU + {BC21E9D0-E865-499B-8395-4067E5E7DB09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC21E9D0-E865-499B-8395-4067E5E7DB09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC21E9D0-E865-499B-8395-4067E5E7DB09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC21E9D0-E865-499B-8395-4067E5E7DB09}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/AppDependencyInjectionExtensions.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/AppDependencyInjectionExtensions.cs new file mode 100644 index 00000000..7b23b667 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/AppDependencyInjectionExtensions.cs @@ -0,0 +1,51 @@ +using Lab.ORM.DynamicField.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.ORM.DynamicField; + +public static class AppDependencyInjectionExtensions +{ + public static void AddAppEnvironment(this IServiceCollection services) + { + services.AddLogging(builder => { builder.AddConsole(); }); + services.AddSingleton(); + } + + public static void AddEntityFramework(this IServiceCollection services) + { + services.AddDbContextFactory((provider, options) => + { + var option = provider.GetService(); + var connectionString = option.EmployeeDbConnectionString; + options.UseNpgsql(connectionString, //只會呼叫一次 + builder => builder.EnableRetryOnFailure( + 10, + TimeSpan.FromSeconds(30), + new List { "57P01" }) + ) + + // .UseLazyLoadingProxies() + // .EnableSensitiveDataLogging() //这将捕获通过迁移发送的更改。 + .LogTo(Console.WriteLine, LogLevel.Information) //这将捕获所有发送到数据库的SQL。 + // .UseLoggerFactory(loggerFactory) + ; + + //.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + }); + } + + // public static void AddEntityFramework(this IServiceCollection services) + // { + // services.AddPooledDbContextFactory((provider, optionsBuilder) => + // { + // var option = provider.GetService(); + // var connectionString = option.EmployeeDbConnectionString; + // var loggerFactory = provider.GetService(); + // optionsBuilder.UseNpgsql(connectionString) + // .UseLoggerFactory(loggerFactory) + // ; + // }); + // } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/AppEnvironmentOption.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/AppEnvironmentOption.cs new file mode 100644 index 00000000..93d18070 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/AppEnvironmentOption.cs @@ -0,0 +1,32 @@ +namespace Lab.ORM.DynamicField; + +public class AppEnvironmentOption +{ + public string EmployeeDbConnectionString + { + get + { + if (string.IsNullOrWhiteSpace(this._employeeDbConnectionString)) + { + this._employeeDbConnectionString = + EnvironmentAssistant.GetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR); + } + + return this._employeeDbConnectionString; + } + set + { + this._employeeDbConnectionString = value; + Environment.SetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR, value); + } + } + + private readonly string EMPLOYEE_DB_CONN_STR = "EMPLOYEE_DB_CONNECTION_STR"; + + private string _employeeDbConnectionString; + + public void Initial() + { + var memberDbConnectionString = this.EmployeeDbConnectionString; + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/Customer.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/Customer.cs new file mode 100644 index 00000000..a0b74e88 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/Customer.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace Lab.ORM.DynamicField; + +public record Customer +{ + public string Name { get; set; } + + public int Age { get; set; } + + public Order[] Orders { get; set; } + + public Product Product { get; set; } +} + +public record Order +{ + // [JsonPropertyName("OrderPrice")] + public decimal Price { get; set; } + + public string ShippingAddress { get; set; } +} + +public record Product +{ + public Guid Id { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/DictionaryStringObjectJsonConverter.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/DictionaryStringObjectJsonConverter.cs new file mode 100644 index 00000000..745438fb --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/DictionaryStringObjectJsonConverter.cs @@ -0,0 +1,114 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lab.ORM.DynamicField; + +public class DictionaryStringObjectJsonConverter : JsonConverter> +{ + public override Dictionary Read(ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException($"JsonTokenType was of type {reader.TokenType}, only objects are supported"); + } + + var results = new Dictionary(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return results; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException("JsonTokenType was not PropertyName"); + } + + var propertyName = reader.GetString(); + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new JsonException("Failed to get property name"); + } + + reader.Read(); + + results.Add(propertyName, this.ReadValue(ref reader, options)); + } + + return results; + } + + public override void Write(Utf8JsonWriter writer, + Dictionary value, + JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (var key in value.Keys) + { + WriteValue(writer, key, value[key], options); + } + + writer.WriteEndObject(); + } + + private object ReadValue(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + if (reader.TryGetDateTimeOffset(out var dateOffset)) + { + return dateOffset; + } + + if (reader.TryGetGuid(out var guid)) + { + return guid; + } + + return reader.GetString(); + case JsonTokenType.False: + case JsonTokenType.True: + return reader.GetBoolean(); + case JsonTokenType.Null: + return null; + case JsonTokenType.Number: + if (reader.TryGetInt64(out var result)) + { + return result; + } + + return reader.GetDecimal(); + case JsonTokenType.StartObject: + return this.Read(ref reader, null, options); + case JsonTokenType.StartArray: + var list = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + list.Add(this.ReadValue(ref reader, options)); + } + + return list; + default: + throw new JsonException($"'{reader.TokenType}' is not supported"); + } + } + + private static void WriteValue(Utf8JsonWriter writer, + string key, + object value, + JsonSerializerOptions options) + { + if (key != null) + { + writer.WritePropertyName(key); + } + + JsonSerializer.Serialize(writer, value, options); + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/Employee.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/Employee.cs new file mode 100644 index 00000000..97b69cfa --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/Employee.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json; + +namespace Lab.ORM.DynamicField.EntityModel +{ + [Table("Employee")] + public class Employee : IDisposable + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Id { get; set; } + + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Remark { get; set; } + + public JsonDocument Profiles { get; set; } + + // [NotMapped] + public Customer Customer + { + get; + set; + + // get => _customer == null ? null : JsonSerializer.Deserialize(_customer); + // set => _customer = JsonSerializer.Serialize(value); + } + + internal string _customer; + + [Required] + public DateTimeOffset CreatedAt { get; set; } + + [Required] + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string ModifiedBy { get; set; } + + public void Dispose() => this.Profiles?.Dispose(); + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/EmployeeDbContext.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..c094a419 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; + +namespace Lab.ORM.DynamicField.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = { false }; + + public virtual DbSet Employees { get; set; } + + // public virtual DbSet Identities { get; set; } + // + // public virtual DbSet OrderHistories { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + } + + //管理索引 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var options = new JsonSerializerOptions(); + modelBuilder.Entity(p => + { + p.Property(p => p.Profiles).HasColumnType("jsonb"); + p.Property(p => p.Customer) + .IsRequired(false) + .HasColumnType("jsonb") + // .HasConversion(p => JsonSerializer.Serialize(p, options), + // p => JsonSerializer.Deserialize(p, options)) + ; + + // p.Property(p => p._customer).HasColumnName("Customer").HasColumnType("jsonb"); + p.Property(p => p.Name).IsRequired().HasMaxLength(50); + p.Property(p => p.CreatedAt).IsRequired(); + p.Property(p => p.CreatedBy).IsRequired(); + p.Property(p => p.ModifiedAt).IsRequired(false); + p.Property(p => p.ModifiedBy).IsRequired(false); + p.Property(p => p.Remark).IsRequired(false); + }); + + modelBuilder.Entity(p => + { + p.HasIndex(e => e.SequenceId) + .IsUnique() + ; + }); + } + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/Identity.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/Identity.cs new file mode 100644 index 00000000..a62f5b10 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/Identity.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ORM.DynamicField.EntityModel +{ + [Table("Identity")] + public class Identity + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Employee_Id { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Remark { get; set; } + + [Required] + public DateTimeOffset CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + [ForeignKey("Employee_Id")] + public virtual Employee Employee { get; set; } + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/OrderHistory.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/OrderHistory.cs new file mode 100644 index 00000000..14415d94 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EntityModel/OrderHistory.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ORM.DynamicField.EntityModel +{ + [Table("OrderHistory")] + public class OrderHistory + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public Guid? Employee_Id { get; set; } + + public string Remark { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Product_Id { get; set; } + + public string Product_Name { get; set; } + + [Required] + public DateTime CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EnvironmentAssistant.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EnvironmentAssistant.cs new file mode 100644 index 00000000..14b3d04c --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/EnvironmentAssistant.cs @@ -0,0 +1,15 @@ +namespace Lab.ORM.DynamicField; + +public class EnvironmentAssistant +{ + public static string GetEnvironmentVariable(string key) + { + var result = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrWhiteSpace(result)) + { + throw new Exception($"the key '{key}' not exist in environment variable"); + } + + return result; + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/JsonDocumentExtensions.cs b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/JsonDocumentExtensions.cs new file mode 100644 index 00000000..7af0b958 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/JsonDocumentExtensions.cs @@ -0,0 +1,41 @@ +using System.Text; +using System.Text.Json; + +namespace Lab.ORM.DynamicField; + +public static class JsonDocumentExtensions +{ + public static T To(this JsonDocument source, + JsonSerializerOptions options = default) + { + return source.Deserialize(options); + } + + public static JsonDocument ToJsonDocument(this T source, + JsonDocumentOptions options = default) + where T : class + { + return JsonDocument.Parse(JsonSerializer.SerializeToUtf8Bytes(source), options); + } + + public static JsonDocument ToJsonDocument(this string source, + JsonDocumentOptions options = default) + { + return JsonDocument.Parse(source, options); + } + + public static string ToJsonString(this JsonDocument source, + JsonWriterOptions options = default) + { + if (source == null) + { + return null; + } + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream, options); + source.WriteTo(writer); + writer.Flush(); + return Encoding.UTF8.GetString(stream.ToArray()); + } +} \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/Lab.ORM.DynamicField.csproj b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/Lab.ORM.DynamicField.csproj new file mode 100644 index 00000000..65c79f52 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/Lab.ORM.DynamicField.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/Makefile b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/Makefile new file mode 100644 index 00000000..cf17f07f --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/Lab.ORM.DynamicField/Makefile @@ -0,0 +1,4 @@ +G1: + echo 'Hello World' +G2: + cmd \ No newline at end of file diff --git a/ORM/Lab.ORM.DynamicField/docker-compose.yml b/ORM/Lab.ORM.DynamicField/docker-compose.yml new file mode 100644 index 00000000..0c353024 --- /dev/null +++ b/ORM/Lab.ORM.DynamicField/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.8" + +services: + db: + image: postgres:12-alpine + environment: + - POSTGRES_PASSWORD=guest + ports: + - 5432:5432 \ No newline at end of file diff --git a/ORM/Linq2Db/Lab.Linq2Db/Lab.UnitTest/EntityModel/CopyMe.SqlServer.generated.cs b/ORM/Linq2Db/Lab.Linq2Db/Lab.UnitTest/EntityModel/CopyMe.SqlServer.generated.cs new file mode 100644 index 00000000..092071d2 --- /dev/null +++ b/ORM/Linq2Db/Lab.Linq2Db/Lab.UnitTest/EntityModel/CopyMe.SqlServer.generated.cs @@ -0,0 +1,216 @@ +//--------------------------------------------------------------------------------------------------- +// +// This code was generated by T4Model template for T4 (https://github.com/linq2db/linq2db). +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//--------------------------------------------------------------------------------------------------- + +#pragma warning disable 1591 + +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +using LinqToDB; +using LinqToDB.Common; +using LinqToDB.Data; +using LinqToDB.DataProvider.SqlServer; +using LinqToDB.Extensions; +using LinqToDB.Mapping; + +namespace Lab.EntityModel +{ + /// + /// Database : LabEmployee2 + /// Data Source : (localdb)\mssqllocaldb + /// Server Version : 13.00.4001 + /// + public partial class LabEmployee2DB : LinqToDB.Data.DataConnection + { + public ITable Employees { get { return this.GetTable(); } } + public ITable Identities { get { return this.GetTable(); } } + public ITable Orders { get { return this.GetTable(); } } + + public LabEmployee2DB() + { + InitDataContext(); + InitMappingSchema(); + } + + public LabEmployee2DB(string configuration) + : base(configuration) + { + InitDataContext(); + InitMappingSchema(); + } + + partial void InitDataContext (); + partial void InitMappingSchema(); + + #region FreeTextTable + + public class FreeTextKey + { + public T Key; + public int Rank; + } + + private static MethodInfo _freeTextTableMethod1 = typeof(LabEmployee2DB).GetMethod("FreeTextTable", new Type[] { typeof(string), typeof(string) }); + + [FreeTextTableExpression] + public ITable> FreeTextTable(string field, string text) + { + return this.GetTable>( + this, + _freeTextTableMethod1, + field, + text); + } + + private static MethodInfo _freeTextTableMethod2 = + typeof(LabEmployee2DB).GetMethods() + .Where(m => m.Name == "FreeTextTable" && m.IsGenericMethod && m.GetParameters().Length == 2) + .Where(m => m.GetParameters()[0].ParameterType.IsGenericTypeEx() && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>)) + .Where(m => m.GetParameters()[1].ParameterType == typeof(string)) + .Single(); + + [FreeTextTableExpression] + public ITable> FreeTextTable(Expression> fieldSelector, string text) + { + return this.GetTable>( + this, + _freeTextTableMethod2, + fieldSelector, + text); + } + + #endregion + } + + [Table(Schema="dbo", Name="Employee")] + public partial class Employee + { + [PrimaryKey, NotNull ] public Guid Id { get; set; } // uniqueidentifier + [Column, Nullable] public string Name { get; set; } // nvarchar(50) + [Column, Nullable] public int? Age { get; set; } // int + [Identity ] public long SequenceId { get; set; } // bigint + [Column, Nullable] public string Remark { get; set; } // nvarchar(50) + + #region Associations + + /// + /// FK_Identity_Employee_Id_BackReference + /// + [Association(ThisKey="Id", OtherKey="EmployeeId", CanBeNull=true, Relationship=Relationship.OneToOne, IsBackReference=true)] + public Identity IdentityId { get; set; } + + /// + /// FK_Order_Employee_id_BackReference + /// + [Association(ThisKey="Id", OtherKey="EmployeeId", CanBeNull=true, Relationship=Relationship.OneToMany, IsBackReference=true)] + public IEnumerable Orderids { get; set; } + + #endregion + } + + [Table(Schema="dbo", Name="Identity")] + public partial class Identity + { + [Column("Employee_Id"), PrimaryKey, NotNull] public Guid EmployeeId { get; set; } // uniqueidentifier + [Column(), NotNull] public string Account { get; set; } // nvarchar(50) + [Column(), NotNull] public string Password { get; set; } // nvarchar(50) + [Column(), Identity ] public long SequenceId { get; set; } // bigint + [Column(), Nullable ] public string Remark { get; set; } // nvarchar(50) + + #region Associations + + /// + /// FK_Identity_Employee_Id + /// + [Association(ThisKey="EmployeeId", OtherKey="Id", CanBeNull=false, Relationship=Relationship.OneToOne, KeyName="FK_Identity_Employee_Id", BackReferenceName="IdentityId")] + public Employee Employee { get; set; } + + #endregion + } + + [Table(Schema="dbo", Name="Order")] + public partial class Order + { + [Column(), PrimaryKey, NotNull] public Guid Id { get; set; } // uniqueidentifier + [Column("Employee_Id"), Nullable ] public Guid? EmployeeId { get; set; } // uniqueidentifier + [Column(), Nullable ] public DateTime? OrderTime { get; set; } // datetime + [Column(), Nullable ] public string Remark { get; set; } // nvarchar(50) + [Column(), Identity ] public long SequenceId { get; set; } // bigint + + #region Associations + + /// + /// FK_Order_Employee_id + /// + [Association(ThisKey="EmployeeId", OtherKey="Id", CanBeNull=true, Relationship=Relationship.ManyToOne, KeyName="FK_Order_Employee_id", BackReferenceName="Orderids")] + public Employee Employee { get; set; } + + #endregion + } + + public static partial class LabEmployee2DBStoredProcedures + { + #region GetAllEmployee + + public static IEnumerable GetAllEmployee(this DataConnection dataConnection) + { + return dataConnection.QueryProc("[dbo].[GetAllEmployee]"); + } + + #endregion + + #region InsertOrUpdateEmployee + + public static int InsertOrUpdateEmployee(this DataConnection dataConnection, Guid? @Id, string @Name, int? @Age, string @Remark) + { + return dataConnection.ExecuteProc("[dbo].[InsertOrUpdateEmployee]", + new DataParameter("@Id", @Id, DataType.Guid), + new DataParameter("@Name", @Name, DataType.NVarChar), + new DataParameter("@Age", @Age, DataType.Int32), + new DataParameter("@Remark", @Remark, DataType.NVarChar)); + } + + #endregion + + #region InsertOrUpdateEmployee2 + + public static int InsertOrUpdateEmployee2(this DataConnection dataConnection, DataTable @EmployeeType) + { + return dataConnection.ExecuteProc("[dbo].[InsertOrUpdateEmployee2]", + new DataParameter("@EmployeeType", @EmployeeType, DataType.Structured){ DbType = "[dbo].[InsertOrUpdateEmployeeType]" }); + } + + #endregion + } + + public static partial class TableExtensions + { + public static Employee Find(this ITable table, Guid Id) + { + return table.FirstOrDefault(t => + t.Id == Id); + } + + public static Identity Find(this ITable table, Guid EmployeeId) + { + return table.FirstOrDefault(t => + t.EmployeeId == EmployeeId); + } + + public static Order Find(this ITable table, Guid Id) + { + return table.FirstOrDefault(t => + t.Id == Id); + } + } +} + +#pragma warning restore 1591 diff --git a/Property Change Tracking/ChangeTrackProperty/ChangeTrackProperty.sln b/Property Change Tracking/ChangeTrackProperty/ChangeTrackProperty.sln new file mode 100644 index 00000000..1c868fe3 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/ChangeTrackProperty.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{1F776B7D-C182-4DC3-BA57-844657BD9AC0}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + Makefile = Makefile + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{483A9EB7-C4AF-4D68-80ED-49277985C760}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Infrastructure.DB", "src\Lab.ChangeTracking.Infrastructure.DB\Lab.ChangeTracking.Infrastructure.DB.csproj", "{A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Domain", "src\Lab.ChangeTracking.Domain\Lab.ChangeTracking.Domain.csproj", "{A1C827F2-683E-470C-A3DE-BD6DD2BE5198}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Domain.UnitTest", "src\Lab.ChangeTracking.Domain.UnitTest\Lab.ChangeTracking.Domain.UnitTest.csproj", "{7066EE2C-28A8-4408-B212-E582608AB967}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Release|Any CPU.Build.0 = Release|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Release|Any CPU.Build.0 = Release|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {7066EE2C-28A8-4408-B212-E582608AB967} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + EndGlobalSection +EndGlobal diff --git a/Property Change Tracking/ChangeTrackProperty/Makefile b/Property Change Tracking/ChangeTrackProperty/Makefile new file mode 100644 index 00000000..cf17f07f --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/Makefile @@ -0,0 +1,4 @@ +G1: + echo 'Hello World' +G2: + cmd \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/docker-compose.yml b/Property Change Tracking/ChangeTrackProperty/docker-compose.yml new file mode 100644 index 00000000..8ecb1567 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.8" + +services: + db-sql: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=pass@w0rd1~ + ports: + - 1433:1433 \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs new file mode 100644 index 00000000..91b6b981 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Unicode; +using EFCore.BulkExtensions; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Lab.MultiTestCase.UnitTest; +using Microsoft.EntityFrameworkCore; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.ChangeTracking.Domain.UnitTest; + +[TestClass] +public class ChangeTrackingUnitTest +{ + private static readonly IAccessContext _accessContext = TestAssistants.AccessContext; + + private static readonly IUUIdProvider _uuIdProvider = TestAssistants.UUIdProvider; + + private static readonly IDbContextFactory s_employeeDbContextFactory = + TestAssistants.EmployeeDbContextFactory; + + private readonly IEmployeeAggregate _employeeAggregate = TestAssistants.EmployeeAggregate; + + private readonly IEmployeeRepository _employeeRepository = TestAssistants.EmployeeRepository; + + private readonly ISystemClock _systemClock = TestAssistants.SystemClock; + + [ClassCleanup] + public static void ClassCleanup() + { + DeleteAllTable(); + } + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + DeleteAllTable(); + } + + [TestMethod] + public void 刪除一筆資料() + { + var fromDb = Insert(); + var employeeEntity = new EmployeeEntity(); + employeeEntity.AsTrackable(fromDb) + .SetDelete() + .AcceptChanges(this._systemClock, _accessContext, _uuIdProvider); + + var count = this._employeeRepository.SaveChangeAsync(employeeEntity).Result; + + Assert.AreEqual(1, count); + var dbContext = s_employeeDbContextFactory.CreateDbContext(); + + var actual = dbContext.Employees + .Where(p => p.Id == fromDb.Id) + .Include(p => p.Identity) + .Include(p => p.Addresses) + .FirstOrDefault() + ; + Assert.AreEqual(null, actual); + } + + [TestMethod] + public void 更新一筆資料() + { + var fromDb = Insert(); + var employeeEntity = new EmployeeEntity(); + employeeEntity.AsTrackable(fromDb) + .SetProfile("小章", 19, "我變了") + .AcceptChanges(this._systemClock, _accessContext, _uuIdProvider); + + var count = this._employeeRepository.SaveChangeAsync(employeeEntity).Result; + + Assert.AreEqual(1, count); + var dbContext = s_employeeDbContextFactory.CreateDbContext(); + + var actual = dbContext.Employees + .Where(p => p.Id == fromDb.Id) + .Include(p => p.Identity) + .Include(p => p.Addresses) + .First() + ; + Assert.AreEqual("小章", actual.Name); + Assert.AreEqual(19, actual.Age); + Assert.AreEqual("我變了", actual.Remark); + } + + [TestMethod] + public void 沒有異動() + { + var fromDb = Insert(); + var employeeEntity = new EmployeeEntity(); + employeeEntity.AsTrackable(fromDb) + .SetProfile("小章", 19, "新來的") + .SetProfile("yao", 18, "編輯") + .AcceptChanges(this._systemClock, _accessContext, _uuIdProvider); + + var count = this._employeeRepository.SaveChangeAsync(employeeEntity).Result; + Assert.AreEqual(0, count); + } + + [TestMethod] + public void 新增一筆資料() + { + var employeeEntity = new EmployeeEntity(); + employeeEntity.New("yao", 10, "新的") + .AcceptChanges(this._systemClock, _accessContext, _uuIdProvider); + + var count = this._employeeRepository.SaveChangeAsync(employeeEntity).Result; + + Assert.AreEqual(1, count); + var dbContext = s_employeeDbContextFactory.CreateDbContext(); + + var actual = dbContext.Employees + .Where(p => p.Id == employeeEntity.Id) + .Include(p => p.Identity) + .Include(p => p.Addresses) + .First() + ; + Assert.AreEqual("yao", actual.Name); + Assert.AreEqual(10, actual.Age); + Assert.AreEqual("新的", actual.Remark); + } + + private static void DeleteAllTable() + { + var dbContext = s_employeeDbContextFactory.CreateDbContext(); + dbContext.Employees.BatchDelete(); + dbContext.Addresses.BatchDelete(); + dbContext.Identity.BatchDelete(); + } + + private static Employee Insert() + { + using var dbContext = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + var employeeId = Guid.NewGuid(); + var toDB = new Employee + { + Id = employeeId, + Age = 18, + Name = "yao", + CreatedAt = DateTimeOffset.Now, + CreatedBy = "Sys", + Remark = "編輯", + Identity = new Identity + { + Employee_Id = employeeId, + Account = "yao", + Password = "123456", + CreatedAt = DateTimeOffset.Now, + CreatedBy = "Sys", + Remark = "編輯" + }, + Addresses = new List
+ { + new() + { + Id = Guid.NewGuid(), + Employee_Id = employeeId, + CreatedAt = DateTimeOffset.Now, + CreatedBy = "sys", + Country = "Taipei", + Street = "Street", + Remark = "修改的" + }, + new() + { + Id = Guid.NewGuid(), + Employee_Id = employeeId, + CreatedAt = DateTimeOffset.Now, + CreatedBy = "sys", + Country = "Taipei", + Street = "Street", + Remark = "刪除的" + } + } + }; + dbContext.Employees.Add(toDB); + dbContext.SaveChanges(); + return toDB; + } + + private static string ToJson(T instance) + { + var serialize = JsonSerializer.Serialize(instance, + new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.Create( + UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs) + }); + return serialize; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj new file mode 100644 index 00000000..e560c2d6 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs new file mode 100644 index 00000000..ca75b47b --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs @@ -0,0 +1,31 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.MultiTestCase.UnitTest; + +[TestClass] +public class MsTestHook +{ + [AssemblyCleanup] + public static void Cleanup() + { + TestAssistants.SetTestEnvironmentVariable(); + var db = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + } + + [AssemblyInitialize] + public static void Setup(TestContext context) + { + TestAssistants.SetTestEnvironmentVariable(); + var db = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + + db.Database.EnsureCreated(); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs new file mode 100644 index 00000000..82708555 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs @@ -0,0 +1,58 @@ +using System; +using Lab.ChangeTracking.Domain; +using Lab.ChangeTracking.Infrastructure.DB; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Lab.MultiTestCase.UnitTest; + +// assistant +internal class TestAssistants +{ + private static IServiceProvider _serviceProvider; + + public static IDbContextFactory EmployeeDbContextFactory => + _serviceProvider.GetService>(); + + public static IEmployeeRepository EmployeeRepository => + _serviceProvider.GetService(); + + public static IEmployeeAggregate EmployeeAggregate => + _serviceProvider.GetService(); + + public static ISystemClock SystemClock => + _serviceProvider.GetService(); + + public static IAccessContext AccessContext => + _serviceProvider.GetService(); + + public static IUUIdProvider UUIdProvider => + _serviceProvider.GetService(); + + static TestAssistants() + { + var services = new ServiceCollection(); + ConfigureTestServices(services); + SetTestEnvironmentVariable(); + } + + public static void ConfigureTestServices(IServiceCollection services) + { + services.AddAppEnvironment(); + services.AddEntityFramework(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + _serviceProvider = services.BuildServiceProvider(); + } + + public static void SetTestEnvironmentVariable() + { + var option = _serviceProvider.GetService(); + option.EmployeeDbConnectionString = + "Data Source=localhost;Initial Catalog=EmployeeDb;Integrated Security=false;User ID=sa;Password=pass@w0rd1~;MultipleActiveResultSets=True;TrustServerCertificate=True"; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/AccessContext.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/AccessContext.cs new file mode 100644 index 00000000..85ba24fb --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/AccessContext.cs @@ -0,0 +1,14 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IAccessContext +{ + public string? GetUserId(); +} + +public class AccessContext : IAccessContext +{ + public string? GetUserId() + { + return "Sys"; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/CommitState.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/CommitState.cs new file mode 100644 index 00000000..bc28b09f --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/CommitState.cs @@ -0,0 +1,8 @@ +namespace Lab.ChangeTracking.Domain; + +public enum CommitState +{ + Unchanged = 0, + Accepted = 1, + Rejected = 2, +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs new file mode 100644 index 00000000..231e5bbc --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs @@ -0,0 +1,29 @@ +namespace Lab.ChangeTracking.Domain; + +public class EmployeeAggregate +{ + private IEmployeeRepository _repository; + private IUUIdProvider _idProvider; + private ISystemClock _systemClock; + private IAccessContext _accessContext; + private EmployeeEntity _employeeEntity; + + public void Create() + { + } + public EmployeeAggregate(IEmployeeRepository repository, + IUUIdProvider idProvider, + ISystemClock systemClock, + IAccessContext accessContext) + { + this._repository = repository; + this._idProvider = idProvider; + this._systemClock = systemClock; + this._accessContext = accessContext; + } + + void Save() + { + + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/AddressEntity.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/AddressEntity.cs new file mode 100644 index 00000000..6b6a7214 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/AddressEntity.cs @@ -0,0 +1,22 @@ +namespace Lab.ChangeTracking.Domain; + +public record AddressEntity +{ + public Guid Id { get; set; } + + public Guid Employee_Id { get; set; } + + public string Country { get; set; } + + public string Street { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string ModifiedBy { get; set; } + + public string Remark { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs new file mode 100644 index 00000000..b40da0a2 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs @@ -0,0 +1,94 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; + +namespace Lab.ChangeTracking.Domain; + +public record EmployeeEntity : EntityBase +{ + public string Name + { + get => this._name; + init => this._name = value; + } + + public int? Age + { + get => this._age; + init => this._age = value; + } + + public string Remark + { + get => this._remark; + init => this._remark = value; + } + + public List Addresses { get; init; } + + public IdentityEntity Identity { get; init; } + + private int? _age; + private string _name; + private string _remark; + + /// + /// 從資料庫查到之後放進去 + /// + /// + /// + public EmployeeEntity AsTrackable(Employee employee) + { + this._changedProperties.Clear(); + this._originalValues.Clear(); + this._entityState = EntityState.Unchanged; + this._commitState = CommitState.Unchanged; + this._id = employee.Id; + this._version = employee.Version; + this._createdAt = employee.CreatedAt; + this._createdBy = employee.CreatedBy; + this._modifiedAt = employee.ModifiedAt; + this._modifiedBy = employee.ModifiedBy; + this._version = employee.Version; + this._name = employee.Name; + this._age = employee.Age; + this._remark = employee.Remark; + + // Addresses = null, + // Identity = null, + + this.AsTrackable(); + return this; + } + + public EmployeeEntity SetDelete() + { + this._entityState = EntityState.Deleted; + return this; + } + + public EmployeeEntity New(string name, int age, string remark = null) + { + this._entityState = EntityState.Added; + this._commitState = CommitState.Unchanged; + this._version = 1; + this._name = name; + this._age = age; + this._remark = remark; + return this; + } + + public override void RejectChanges() + { + throw new NotImplementedException(); + } + + public EmployeeEntity SetProfile(string name, int age, string remark = null) + { + this._name = name; + this._age = age; + this._remark = remark; + this.ChangeTrack(nameof(this.Name), name); + this.ChangeTrack(nameof(this.Age), age); + this.ChangeTrack(nameof(this.Remark), remark); + return this; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs new file mode 100644 index 00000000..bcdde865 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs @@ -0,0 +1,20 @@ +namespace Lab.ChangeTracking.Domain; + +public record IdentityEntity +{ + public Guid Employee_Id { get; set; } + + public string Account { get; set; } + + public string Password { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string? ModifiedBy { get; set; } + + public virtual string Remark { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs new file mode 100644 index 00000000..224c9362 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs @@ -0,0 +1,6 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IEmployeeAggregate +{ + Task ModifyFlowAsync(EmployeeEntity employee, CancellationToken cancel = default); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs new file mode 100644 index 00000000..2791fd8f --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs @@ -0,0 +1,84 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; + +namespace Lab.ChangeTracking.Domain; + +public class EmployeeRepository : IEmployeeRepository +{ + private readonly IDbContextFactory _employeeDbContextFactory; + + public EmployeeRepository(IDbContextFactory memberContextFactory) + { + this._employeeDbContextFactory = memberContextFactory; + } + + public async Task SaveChangeAsync(EmployeeEntity srcEmployee, + IEnumerable excludeProperties = null, + CancellationToken cancel = default) + { + if (srcEmployee.CommitState != CommitState.Accepted) + { + throw new Exception($"{nameof(srcEmployee)} 尚未核准,不得儲存"); + } + + await using var dbContext = await this._employeeDbContextFactory.CreateDbContextAsync(cancel); + switch (srcEmployee.EntityState) + { + case EntityState.Added: + ApplyAdd(dbContext, srcEmployee); + break; + case EntityState.Modified: + ApplyModify(dbContext, srcEmployee, excludeProperties); + + break; + case EntityState.Deleted: + ApplyDelete(srcEmployee, dbContext); + + break; + + case EntityState.Unchanged: + return 0; + default: + throw new ArgumentOutOfRangeException(); + } + + return await dbContext.SaveChangesAsync(cancel); + } + + private static void ApplyDelete(EmployeeEntity srcEmployee, EmployeeDbContext dbContext) + { + dbContext.Set().Remove(new Employee() { Id = srcEmployee.Id }); + } + + private static void ApplyAdd(EmployeeDbContext dbContext, EmployeeEntity srcEmployee) + { + dbContext.Set().Add(srcEmployee.To()); + } + + private static void ApplyModify(EmployeeDbContext dbContext, + EmployeeEntity srcEmployee, + IEnumerable excludeProperties = null) + { + var destEmployee = new Employee() + { + Id = srcEmployee.Id + }; + + dbContext.Set().Attach(destEmployee); + var employeeEntry = dbContext.Entry(destEmployee); + + foreach (var property in srcEmployee.GetChangedProperties()) + { + var propertyName = property.Key; + var value = property.Value; + if (excludeProperties != null + && excludeProperties.Any(p => p == propertyName)) + { + continue; + } + + dbContext.Entry(destEmployee).Property(propertyName).CurrentValue = value; + employeeEntry.Property(propertyName).IsModified = true; + } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs new file mode 100644 index 00000000..779211fc --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs @@ -0,0 +1,8 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IEmployeeRepository +{ + Task SaveChangeAsync(EmployeeEntity employee, + IEnumerable excludeProperties = null, + CancellationToken cancel = default); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/TypeConverterExtensions.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/TypeConverterExtensions.cs new file mode 100644 index 00000000..4b0d422d --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/TypeConverterExtensions.cs @@ -0,0 +1,144 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; + +namespace Lab.ChangeTracking.Domain; + +public static class TypeConverterExtensions +{ + public static Employee To(this EmployeeEntity srcEmployee) + { + if (srcEmployee == null) + { + return null; + } + + return new Employee + { + Id = srcEmployee.Id, + Name = srcEmployee.Name, + Age = srcEmployee.Age, + Version = srcEmployee.Version, + Remark = srcEmployee.Remark, + Addresses = srcEmployee.Addresses != null ? srcEmployee.Addresses.To().ToList() : null, + Identity = srcEmployee.Identity.To(), + CreatedAt = srcEmployee.CreatedAt, + CreatedBy = srcEmployee.CreatedBy, + ModifiedAt = srcEmployee.ModifiedAt, + ModifiedBy = srcEmployee.ModifiedBy + }; + } + + public static EmployeeEntity To(this Employee srcEmployee) + { + if (srcEmployee == null) + { + return null; + } + + return new EmployeeEntity + { + Id = srcEmployee.Id, + Name = srcEmployee.Name, + Age = srcEmployee.Age, + Version = srcEmployee.Version, + Remark = srcEmployee.Remark, + Addresses = srcEmployee.Addresses.To()?.ToList(), + Identity = srcEmployee.Identity.To(), + CreatedAt = srcEmployee.CreatedAt, + CreatedBy = srcEmployee.CreatedBy, + ModifiedAt = srcEmployee.ModifiedAt, + ModifiedBy = srcEmployee.ModifiedBy + }; + } + + public static Identity To(this IdentityEntity srcIdentity) + { + if (srcIdentity == null) + { + return null; + } + + return new Identity + { + Employee_Id = srcIdentity.Employee_Id, + Account = srcIdentity.Account, + Password = srcIdentity.Password, + Remark = srcIdentity.Remark, + CreatedAt = srcIdentity.CreatedAt, + CreatedBy = srcIdentity.CreatedBy, + ModifiedAt = srcIdentity.ModifiedAt, + ModifiedBy = srcIdentity.ModifiedBy + }; + } + + public static IdentityEntity To(this Identity srcIdentity) + { + if (srcIdentity == null) + { + return null; + } + + return new IdentityEntity + { + Employee_Id = srcIdentity.Employee_Id, + Account = srcIdentity.Account, + Password = srcIdentity.Password, + Remark = srcIdentity.Remark, + CreatedAt = srcIdentity.CreatedAt, + CreatedBy = srcIdentity.CreatedBy, + ModifiedAt = srcIdentity.ModifiedAt, + ModifiedBy = srcIdentity.ModifiedBy + }; + } + + public static Address To(this AddressEntity srcAddress) + { + if (srcAddress == null) + { + return null; + } + + return new Address + { + Id = srcAddress.Id, + Employee_Id = srcAddress.Employee_Id, + Country = srcAddress.Country, + Street = srcAddress.Street, + CreatedAt = srcAddress.CreatedAt, + CreatedBy = srcAddress.CreatedBy, + ModifiedAt = srcAddress.ModifiedAt, + ModifiedBy = srcAddress.ModifiedBy, + Remark = srcAddress.Remark + }; + } + + public static AddressEntity To(this Address srcAddress) + { + if (srcAddress == null) + { + return null; + } + + return new AddressEntity + { + Id = srcAddress.Id, + Employee_Id = srcAddress.Employee_Id, + Country = srcAddress.Country, + Street = srcAddress.Street, + CreatedAt = srcAddress.CreatedAt, + CreatedBy = srcAddress.CreatedBy, + ModifiedAt = srcAddress.ModifiedAt, + ModifiedBy = srcAddress.ModifiedBy, + Remark = srcAddress.Remark + }; + } + + public static IEnumerable
To(this IEnumerable srcProfiles) + { + return srcProfiles?.Select(p => p?.To()); + } + + public static IEnumerable To(this IEnumerable
srcProfiles) + { + return srcProfiles?.Select(p => p?.To()); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EntityBase.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EntityBase.cs new file mode 100644 index 00000000..f0f0dfdf --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EntityBase.cs @@ -0,0 +1,168 @@ +using System.Collections; + +namespace Lab.ChangeTracking.Domain; + +public abstract record EntityBase : IChangeTrackable +{ + public Guid Id + { + get => this._id; + init => this._id = value; + } + + protected readonly Dictionary _changedProperties = new(); + protected readonly Dictionary _originalValues = new(); + protected CommitState _commitState; + protected DateTimeOffset _createdAt; + protected string _createdBy; + protected EntityState _entityState; + protected Guid _id; + protected DateTimeOffset? _modifiedAt; + protected string? _modifiedBy; + protected int _version; + + public EntityBase AsTrackable() + { + this.Validate(); + + // this._entityState = EntityState.Added; + // this._commitState = CommitState.Unchanged; + // this._version = 1; + var properties = this.GetType().GetProperties(); + foreach (var property in properties) + { + this._originalValues.Add(property.Name, property.GetValue(this)); + } + + return this; + } + + public (Error err, bool changed) AcceptChanges(ISystemClock systemClock, + IAccessContext accessContext, + IUUIdProvider idProvider) + { + this.Validate(); + + this._commitState = CommitState.Accepted; + var (now, accessUserId) = (systemClock.GetNow(), accessContext.GetUserId()); + + if (this.EntityState == EntityState.Unchanged) + { + return (null, false); + } + + if (this.EntityState == EntityState.Added) + { + this._id = idProvider.GenerateId(); + this._createdAt = now; + this._createdBy = accessUserId; + this._version = 1; + } + else + { + this._version = this._version++; + } + + this._modifiedAt = now; + this._modifiedBy = accessUserId; + + // this._entityState = EntityState.Submitted; + + return (null, true); + } + + public abstract void RejectChanges(); + + public Dictionary GetChangedProperties() + { + return this._changedProperties; + } + + public EntityState EntityState + { + get => this._entityState; + init => this._entityState = value; + } + + public CommitState CommitState + { + get => this._commitState; + init => this._commitState = value; + } + + public int Version + { + get => this._version; + init => this._version = value; + } + + public Dictionary GetOriginalValues() + { + return this._originalValues; + } + + public DateTimeOffset CreatedAt + { + get => this._createdAt; + init => this._createdAt = value; + } + + public string? CreatedBy + { + get => this._createdBy; + init => this._createdBy = value; + } + + public DateTimeOffset? ModifiedAt + { + get => this._modifiedAt; + init => this._modifiedAt = value; + } + + public string? ModifiedBy + { + get => this._modifiedBy; + init => this._modifiedBy = value; + } + + public void ChangeTrack(string propertyName, object value) + { + this.Validate(); + + var changes = this._changedProperties; + var originals = this._originalValues; + if (originals.Count <= 0) + { + throw new Exception("尚未啟用追蹤"); + } + + if (changes.ContainsKey(propertyName) == false) + { + if (originals[propertyName] != value) + { + changes.Add(propertyName, value); + this._entityState = EntityState.Modified; + } + } + else + { + if (originals[propertyName].ToString() == value.ToString()) + { + changes.Remove(propertyName); + } + } + + if (changes.Count <= 0) + { + this._entityState = EntityState.Unchanged; + } + } + + private void Validate() + { + if (this.CommitState == CommitState.Accepted) + { + throw new Exception("已經同意,無法再進行修改"); + } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EntityState.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EntityState.cs new file mode 100644 index 00000000..7fff1298 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/EntityState.cs @@ -0,0 +1,9 @@ +namespace Lab.ChangeTracking.Domain; + +public enum EntityState +{ + Unchanged = 0, + Added = 1, + Modified = 2, + Deleted = 3, +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/Error.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/Error.cs new file mode 100644 index 00000000..aa8f1b7a --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/Error.cs @@ -0,0 +1,3 @@ +namespace Lab.ChangeTracking.Domain; + +public record Error(T Code, object Message); \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeContent.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeContent.cs new file mode 100644 index 00000000..6d899ee9 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeContent.cs @@ -0,0 +1,8 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IChangeContent +{ + Dictionary GetChangedProperties(); + + Dictionary GetOriginalValues(); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeState.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeState.cs new file mode 100644 index 00000000..90225b7f --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeState.cs @@ -0,0 +1,10 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IChangeState +{ + EntityState EntityState { get; init; } + + CommitState CommitState { get; init; } + + int Version { get; init; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeTime.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeTime.cs new file mode 100644 index 00000000..605ff333 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeTime.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; + +namespace Lab.ChangeTracking.Domain; + +public interface IChangeTime +{ + DateTimeOffset CreatedAt { get; init; } + + string CreatedBy { get; init; } + + DateTimeOffset? ModifiedAt { get; init; } + + string? ModifiedBy { get; init; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeTrackable.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeTrackable.cs new file mode 100644 index 00000000..ae42e1f4 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IChangeTrackable.cs @@ -0,0 +1,12 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IChangeTrackable : IChangeContent, IChangeTime, IChangeState +{ + public (Error err, bool changed) AcceptChanges(ISystemClock systemClock, + IAccessContext accessContext, + IUUIdProvider idProvider); + + void ChangeTrack(string propertyName, object value); + + void RejectChanges(); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/ISystemClock.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/ISystemClock.cs new file mode 100644 index 00000000..50076e0d --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/ISystemClock.cs @@ -0,0 +1,14 @@ +namespace Lab.ChangeTracking.Domain; + +public interface ISystemClock +{ + DateTimeOffset GetNow(); +} + +public class SystemClock : ISystemClock +{ + public DateTimeOffset GetNow() + { + return DateTimeOffset.Now; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IUUIdProvider.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IUUIdProvider.cs new file mode 100644 index 00000000..a58ce071 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/IUUIdProvider.cs @@ -0,0 +1,14 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IUUIdProvider +{ + Guid GenerateId(); +} + +public class UUIdProvider : IUUIdProvider +{ + public Guid GenerateId() + { + return Guid.NewGuid(); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj new file mode 100644 index 00000000..a7984ff8 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + enable + enable + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs new file mode 100644 index 00000000..332cf598 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs @@ -0,0 +1,48 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.ChangeTracking.Infrastructure.DB; + +public static class AppDependencyInjectionExtensions +{ + public static void AddAppEnvironment(this IServiceCollection services) + { + services.AddLogging(builder => { builder.AddConsole(); }); + services.AddSingleton(); + } + + public static void AddEntityFramework(this IServiceCollection services) + { + services.AddPooledDbContextFactory((provider, optionsBuilder) => + { + var option = provider.GetService(); + var connectionString = option.EmployeeDbConnectionString; + var loggerFactory = provider.GetService(); + optionsBuilder.UseSqlServer(connectionString) + .UseLoggerFactory(loggerFactory) + .LogTo(Console.WriteLine) + .EnableSensitiveDataLogging() + ; + }); + + // services.AddPooledDbContextFactory((provider, options) => + // { + // var option = provider.GetService(); + // var loggerFactory = provider.GetService(); + // options.UseNpgsql( + // option.MemberDbConnectionString, //只會呼叫一次 + // builder => + // builder.EnableRetryOnFailure( + // 10, + // TimeSpan.FromSeconds(30), + // new List { "57P01" })) + // + // // .UseLazyLoadingProxies() + // .UseSnakeCaseNamingConvention() + // .UseLoggerFactory(loggerFactory) + // ; + // }); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs new file mode 100644 index 00000000..d93a85cc --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs @@ -0,0 +1,32 @@ +namespace Lab.ChangeTracking.Infrastructure.DB; + +public class AppEnvironmentOption +{ + public string EmployeeDbConnectionString + { + get + { + if (string.IsNullOrWhiteSpace(this._employeeDbConnectionString)) + { + this._employeeDbConnectionString = + EnvironmentAssistant.GetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR); + } + + return this._employeeDbConnectionString; + } + set + { + this._employeeDbConnectionString = value; + Environment.SetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR, value); + } + } + + private readonly string EMPLOYEE_DB_CONN_STR = "EMPLOYEE_DB_CONNECTION_STR"; + + private string _employeeDbConnectionString; + + public void Initial() + { + var memberDbConnectionString = this.EmployeeDbConnectionString; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Address.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Address.cs new file mode 100644 index 00000000..cdddbfa0 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Address.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel; + +public class Address +{ + public Guid Id { get; set; } + + public Guid Employee_Id { get; set; } + + public Employee Employee { get; set; } + + public string Country { get; set; } + + public string Street { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string ModifiedBy { get; set; } + + public string Remark { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int SequenceId { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs new file mode 100644 index 00000000..42bc21b0 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class Employee + { + public Guid Id { get; set; } + + public int Version { get; set; } + + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + public IList
Addresses { get; set; } + + public Identity Identity { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string ModifiedBy { get; set; } + + public string Remark { get; set; } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..9ecfbc28 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,120 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = { false }; + + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identity { get; set; } + + public virtual DbSet
Addresses { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + // 改用 CI 執行 Migrate + + // if (s_migrated[0]) + // { + // return; + // } + // + // lock (s_migrated) + // { + // if (s_migrated[0] == false) + // { + // var sqlOptions = options.FindExtension(); + // if (sqlOptions != null) + // { + // this.Database.Migrate(); + // } + // + // s_migrated[0] = true; + // } + // } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new EmployeeConfiguration()); + modelBuilder.ApplyConfiguration(new IdentityConfiguration()); + modelBuilder.ApplyConfiguration(new AddressConfiguration()); + } + + internal class EmployeeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Employee"); + builder.HasKey(p => p.Id) + .IsClustered(false); + + builder.Property(p => p.Name).IsRequired(); + builder.Property(p => p.CreatedBy).IsRequired(); + builder.Property(p => p.CreatedAt).IsRequired(); + builder.Property(p => p.ModifiedAt).IsRequired(false); + builder.Property(p => p.ModifiedBy).IsRequired(false); + builder.Property(p => p.Remark).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + + private class IdentityConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Identity"); + builder.HasKey(p => p.Employee_Id).IsClustered(false); + builder.HasOne(e => e.Employee) + .WithOne(p => p.Identity) + .HasForeignKey(p => p.Employee_Id) + .OnDelete(DeleteBehavior.Cascade) + ; + + builder.Property(p => p.Account).IsRequired(); + builder.Property(p => p.Password).IsRequired(); + builder.Property(p => p.CreatedBy).IsRequired(); + builder.Property(p => p.CreatedAt).IsRequired(); + builder.Property(p => p.ModifiedAt).IsRequired(false); + builder.Property(p => p.ModifiedBy).IsRequired(false); + builder.Property(p => p.Remark).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + + private class AddressConfiguration : IEntityTypeConfiguration
+ { + public void Configure(EntityTypeBuilder
builder) + { + builder.ToTable("Address"); + builder.HasKey(p => p.Id).IsClustered(false); + builder.HasOne(e => e.Employee) + .WithMany(p => p.Addresses) + .HasForeignKey(p => p.Employee_Id) + .OnDelete(DeleteBehavior.Cascade); + + builder.Property(p => p.Country).IsRequired(); + builder.Property(p => p.Street).IsRequired(); + builder.Property(p => p.CreatedBy).IsRequired(); + builder.Property(p => p.CreatedAt).IsRequired(); + builder.Property(p => p.ModifiedBy).IsRequired(false); + builder.Property(p => p.ModifiedAt).IsRequired(false); + builder.Property(p => p.Remark).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs new file mode 100644 index 00000000..e9e2f6d9 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class Identity + { + public Guid Employee_Id { get; set; } + + // [ForeignKey("Employee_Id")] + public virtual Employee Employee { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Remark { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string? ModifiedBy { get; set; } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs new file mode 100644 index 00000000..b115d8a2 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs @@ -0,0 +1,15 @@ +namespace Lab.ChangeTracking.Infrastructure.DB; + +public class EnvironmentAssistant +{ + public static string GetEnvironmentVariable(string key) + { + var result = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrWhiteSpace(result)) + { + throw new Exception($"the key '{key}' not exist in environment variable"); + } + + return result; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj new file mode 100644 index 00000000..9fcdb0d8 --- /dev/null +++ b/Property Change Tracking/ChangeTrackProperty/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + + + + + + + + + diff --git a/Property Change Tracking/ChangeTrackVersion/ChangeTrackVersion.sln b/Property Change Tracking/ChangeTrackVersion/ChangeTrackVersion.sln new file mode 100644 index 00000000..1c868fe3 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/ChangeTrackVersion.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{1F776B7D-C182-4DC3-BA57-844657BD9AC0}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + Makefile = Makefile + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{483A9EB7-C4AF-4D68-80ED-49277985C760}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Infrastructure.DB", "src\Lab.ChangeTracking.Infrastructure.DB\Lab.ChangeTracking.Infrastructure.DB.csproj", "{A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Domain", "src\Lab.ChangeTracking.Domain\Lab.ChangeTracking.Domain.csproj", "{A1C827F2-683E-470C-A3DE-BD6DD2BE5198}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Domain.UnitTest", "src\Lab.ChangeTracking.Domain.UnitTest\Lab.ChangeTracking.Domain.UnitTest.csproj", "{7066EE2C-28A8-4408-B212-E582608AB967}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Release|Any CPU.Build.0 = Release|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Release|Any CPU.Build.0 = Release|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {7066EE2C-28A8-4408-B212-E582608AB967} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + EndGlobalSection +EndGlobal diff --git a/Property Change Tracking/ChangeTrackVersion/Makefile b/Property Change Tracking/ChangeTrackVersion/Makefile new file mode 100644 index 00000000..cf17f07f --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/Makefile @@ -0,0 +1,4 @@ +G1: + echo 'Hello World' +G2: + cmd \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/docker-compose.yml b/Property Change Tracking/ChangeTrackVersion/docker-compose.yml new file mode 100644 index 00000000..8ecb1567 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.8" + +services: + db-sql: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=pass@w0rd1~ + ports: + - 1433:1433 \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs new file mode 100644 index 00000000..8c989b04 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Unicode; +using EFCore.BulkExtensions; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Lab.MultiTestCase.UnitTest; +using Microsoft.EntityFrameworkCore; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.ChangeTracking.Domain.UnitTest; + +[TestClass] +public class ChangeTrackingUnitTest +{ + private static readonly IAccessContext _accessContext = TestAssistants.AccessContext; + + private static readonly IUUIdProvider _uuIdProvider = TestAssistants.UUIdProvider; + + private static readonly IDbContextFactory s_employeeDbContextFactory = + TestAssistants.EmployeeDbContextFactory; + + private readonly EmployeeAggregate _employeeAggregate = TestAssistants.EmployeeAggregate; + + private readonly IEmployeeRepository _employeeRepository = TestAssistants.EmployeeRepository; + + private readonly ISystemClock _systemClock = TestAssistants.SystemClock; + + [ClassCleanup] + public static void ClassCleanup() + { + DeleteAllTable(); + } + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + DeleteAllTable(); + } + + [TestMethod] + public void 刪除一筆資料() + { + // var fromDb = Insert(); + // var employeeEntity = new EmployeeValueObject(); + // employeeEntity.AsTrackable(fromDb) + // .SetDelete() + // .AcceptChanges(this._systemClock, _accessContext, _uuIdProvider); + // + // var count = this._employeeRepository.SaveChangeAsync(employeeEntity).Result; + // + // Assert.AreEqual(1, count); + // var dbContext = s_employeeDbContextFactory.CreateDbContext(); + // + // var actual = dbContext.Employees + // .Where(p => p.Id == fromDb.Id) + // .Include(p => p.Identity) + // .Include(p => p.Addresses) + // .FirstOrDefault() + // ; + // Assert.AreEqual(null, actual); + } + + [TestMethod] + public void 更新一筆資料() + { + var fromDb = Insert(); + var target = this._employeeAggregate; + target.SetEntity(fromDb.To()) + .SetProfile("小章", 28); + target.AcceptChanges(); + var result = target.CommitChangeAsync().Result; + } + + [TestMethod] + public void 沒有異動() + { + // var fromDb = Insert(); + // var employeeEntity = new EmployeeValueObject(); + // employeeEntity.AsTrackable(fromDb) + // .SetProfile("小章", 19, "新來的") + // .SetProfile("yao", 18, "編輯") + // .AcceptChanges(this._systemClock, _accessContext, _uuIdProvider); + // + // var count = this._employeeRepository.SaveChangeAsync(employeeEntity).Result; + // Assert.AreEqual(0, count); + } + + [TestMethod] + public void 新增() + { + var target = this._employeeAggregate; + target.NewEmployee("yao", 18); + target.AcceptChanges(); + var result = target.CommitChangeAsync().Result; + } + + [TestMethod] + public void 新增一筆資料() + { + // var employeeEntity = new EmployeeValueObject(); + // employeeEntity.New("yao", 10, "新的") + // .AcceptChanges(this._systemClock, _accessContext, _uuIdProvider); + // + // var count = this._employeeRepository.SaveChangeAsync(employeeEntity).Result; + // + // Assert.AreEqual(1, count); + // var dbContext = s_employeeDbContextFactory.CreateDbContext(); + // + // var actual = dbContext.Employees + // .Where(p => p.Id == employeeEntity.Id) + // .Include(p => p.Identity) + // .Include(p => p.Addresses) + // .First() + // ; + // Assert.AreEqual("yao", actual.Name); + // Assert.AreEqual(10, actual.Age); + // Assert.AreEqual("新的", actual.Remark); + } + + private static void DeleteAllTable() + { + var dbContext = s_employeeDbContextFactory.CreateDbContext(); + dbContext.Employees.BatchDelete(); + dbContext.Addresses.BatchDelete(); + dbContext.Identity.BatchDelete(); + } + + private static Employee Insert() + { + using var dbContext = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + var employeeId = Guid.NewGuid(); + var toDB = new Employee + { + Id = employeeId, + Age = 18, + Name = "yao", + CreatedAt = DateTimeOffset.Now, + CreatedBy = "Sys", + Remark = "編輯", + Identity = new Identity + { + Employee_Id = employeeId, + Account = "yao", + Password = "123456", + CreatedAt = DateTimeOffset.Now, + CreatedBy = "Sys", + Remark = "編輯" + }, + Addresses = new List
+ { + new() + { + Id = Guid.NewGuid(), + Employee_Id = employeeId, + CreatedAt = DateTimeOffset.Now, + CreatedBy = "sys", + Country = "Taipei", + Street = "Street", + Remark = "修改的" + }, + new() + { + Id = Guid.NewGuid(), + Employee_Id = employeeId, + CreatedAt = DateTimeOffset.Now, + CreatedBy = "sys", + Country = "Taipei", + Street = "Street", + Remark = "刪除的" + } + } + }; + dbContext.Employees.Add(toDB); + dbContext.SaveChanges(); + return toDB; + } + + private static string ToJson(T instance) + { + var serialize = JsonSerializer.Serialize(instance, + new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.Create( + UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs) + }); + return serialize; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj new file mode 100644 index 00000000..fbfda1f2 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs new file mode 100644 index 00000000..ca75b47b --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs @@ -0,0 +1,31 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.MultiTestCase.UnitTest; + +[TestClass] +public class MsTestHook +{ + [AssemblyCleanup] + public static void Cleanup() + { + TestAssistants.SetTestEnvironmentVariable(); + var db = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + } + + [AssemblyInitialize] + public static void Setup(TestContext context) + { + TestAssistants.SetTestEnvironmentVariable(); + var db = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + + db.Database.EnsureCreated(); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs new file mode 100644 index 00000000..1fb69070 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs @@ -0,0 +1,59 @@ +using System; +using Lab.ChangeTracking.Domain; +using Lab.ChangeTracking.Infrastructure.DB; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Lab.MultiTestCase.UnitTest; + +// assistant +internal class TestAssistants +{ + private static IServiceProvider _serviceProvider; + + public static IDbContextFactory EmployeeDbContextFactory => + _serviceProvider.GetService>(); + + public static IEmployeeRepository EmployeeRepository => + _serviceProvider.GetService(); + + public static EmployeeAggregate EmployeeAggregate => + _serviceProvider.GetService(); + + public static ISystemClock SystemClock => + _serviceProvider.GetService(); + + public static IAccessContext AccessContext => + _serviceProvider.GetService(); + + public static IUUIdProvider UUIdProvider => + _serviceProvider.GetService(); + + static TestAssistants() + { + var services = new ServiceCollection(); + ConfigureTestServices(services); + SetTestEnvironmentVariable(); + } + + public static void ConfigureTestServices(IServiceCollection services) + { + services.AddAppEnvironment(); + services.AddEntityFramework(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + _serviceProvider = services.BuildServiceProvider(); + } + + public static void SetTestEnvironmentVariable() + { + var option = _serviceProvider.GetService(); + option.EmployeeDbConnectionString = + "Data Source=localhost;Initial Catalog=EmployeeDb;Integrated Security=false;User ID=sa;Password=pass@w0rd1~;MultipleActiveResultSets=True;TrustServerCertificate=True"; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/AccessContext.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/AccessContext.cs new file mode 100644 index 00000000..85ba24fb --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/AccessContext.cs @@ -0,0 +1,14 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IAccessContext +{ + public string? GetUserId(); +} + +public class AccessContext : IAccessContext +{ + public string? GetUserId() + { + return "Sys"; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/ChangeState.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/ChangeState.cs new file mode 100644 index 00000000..34f040f5 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/ChangeState.cs @@ -0,0 +1,8 @@ +namespace Lab.ChangeTracking.Domain; + +public enum ChangeState +{ + Added = 0, + Modified = 1, + Deleted = 2, +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/CommitState.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/CommitState.cs new file mode 100644 index 00000000..650245f3 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/CommitState.cs @@ -0,0 +1,9 @@ +namespace Lab.ChangeTracking.Domain; + +public enum CommitState +{ + Unchanged = 0, + Accepted = 1, + Rejected = 2, + Commited = 3 +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs new file mode 100644 index 00000000..e72ad508 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs @@ -0,0 +1,187 @@ +using ChangeTracking; + +namespace Lab.ChangeTracking.Domain; + +public class EmployeeAggregate +{ + public CommitState CommitState { get; private set; } + + private readonly IAccessContext _accessContext; + private readonly IUUIdProvider _idProvider; + + private readonly EmployeeRepository _repository; + private readonly ISystemClock _systemClock; + + private EmployeeEntity _instance; + + public EmployeeAggregate(IUUIdProvider idProvider, + ISystemClock systemClock, + IAccessContext accessContext, + EmployeeRepository repository) + { + this._idProvider = idProvider; + this._systemClock = systemClock; + this._accessContext = accessContext; + this._repository = repository; + } + + public EmployeeAggregate AcceptChanges() + { + if (this.CommitState == CommitState.Accepted) + { + throw new Exception("已接受核准"); + } + + this.CommitState = CommitState.Accepted; + var trackable = this._instance.CastToIChangeTrackable(); + if (trackable.IsChanged) + { + this._instance.Version++; + } + else + { + this._instance.Version = 1; + } + + return this; + } + + public EmployeeAggregate AddAddress(AddressEntity instance) + { + this.ValidateAcceptedState(); + var (when, who) = (this._systemClock.GetNow(), this._accessContext.GetUserId()); + instance.Id = this._idProvider.GenerateId(); + instance.CreatedAt = when; + instance.CreatedBy = who; + + this._instance.Addresses.Add(instance); + return this; + } + + public async Task CommitChangeAsync(CancellationToken cancel = default) + { + if (this.CommitState != CommitState.Accepted) + { + throw new Exception("未被認可"); + } + + var changeCount = 0; + switch (this._instance.EntityState) + { + case ChangeState.Added: + changeCount = await this._repository.InsertEmployeeAsync(this._instance, cancel); + + break; + case ChangeState.Modified: + changeCount = await this._repository.SaveChangesAsync(this._instance, cancel); + + break; + case ChangeState.Deleted: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + this.CommitState = CommitState.Unchanged; + return changeCount; + } + + public Guid GetEmployeeId() + { + return this._instance.Id; + } + + public EmployeeAggregate NewAddress(Guid employeeId, AddressEntity source) + { + this.CommitState = CommitState.Unchanged; + + var target = this._instance + .Addresses + .Where(p => p.Employee_Id == source.Employee_Id) + .FirstOrDefault(); + + if (target == null) + { + return this; + } + + var (when, who) = (this._systemClock.GetNow(), this._accessContext.GetUserId()); + + target.Id = source.Id; + target.Country = source.Country; + target.Street = source.Street; + target.Remark = source.Remark; + target.EntityState = ChangeState.Added; + target.ModifiedBy = who; + target.ModifiedAt = when; + return this; + } + + public Guid NewEmployee(string name, + int age, + string remark = null) + { + this.CommitState = CommitState.Unchanged; + + var (when, who) = (this._systemClock.GetNow(), this._accessContext.GetUserId()); + + this._instance = new EmployeeEntity + { + Id = this._idProvider.GenerateId(), + EntityState = ChangeState.Added, + Name = name, + Age = age, + Remark = remark, + CreatedAt = when, + CreatedBy = who, + ModifiedAt = when, + ModifiedBy = who, + }.AsTrackable(); + + return this._instance.Id; + } + + public EmployeeAggregate SetEntity(EmployeeEntity instance) + { + this.CommitState = CommitState.Unchanged; + + this._instance = instance.AsTrackable(); + this._instance.EntityState = ChangeState.Modified; + + return this; + } + + public void SetProfile(string name, + int age, + string remark = null) + { + this.ValidateAcceptedState(); + this.ValidateAddState(); + var (when, who) = + (this._systemClock.GetNow(), this._accessContext.GetUserId()); + this._instance.ModifiedBy = who; + this._instance.ModifiedAt = when; + + this._instance.Age = age; + this._instance.Name = name; + this._instance.Remark = remark; + } + + private void ValidateAcceptedState() + { + if (this.CommitState == CommitState.Accepted) + { + throw new Exception("已接受核准,不能改變狀態"); + } + + this.ValidateAddState(); + } + + private void ValidateAddState() + { + if (this._instance.EntityState == ChangeState.Added) + { + throw new Exception("Add 狀態不能異動"); + } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/AddressEntity.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/AddressEntity.cs new file mode 100644 index 00000000..0bdc4ad2 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/AddressEntity.cs @@ -0,0 +1,24 @@ +namespace Lab.ChangeTracking.Domain; + +public record AddressEntity +{ + public virtual Guid Id { get; set; } + + public virtual Guid Employee_Id { get; set; } + + public virtual ChangeState EntityState { get; set; } + + public virtual string Country { get; set; } + + public virtual string Street { get; set; } + + public virtual DateTimeOffset CreatedAt { get; set; } + + public virtual string CreatedBy { get; set; } + + public virtual DateTimeOffset? ModifiedAt { get; set; } + + public virtual string ModifiedBy { get; set; } + + public virtual string Remark { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs new file mode 100644 index 00000000..0bafc2b1 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs @@ -0,0 +1,30 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; + +namespace Lab.ChangeTracking.Domain; + +public record EmployeeEntity +{ + public virtual Guid Id { get; set; } + + public virtual ChangeState EntityState { get; set; } + + public virtual string Name { get; set; } + + public virtual int? Age { get; set; } + + public virtual string Remark { get; set; } + + public virtual List Addresses { get; set; } = new(); + + public virtual IdentityEntity Identity { get; set; } + + public virtual string ModifiedBy { get; set; } + + public virtual DateTimeOffset? ModifiedAt { get; set; } + + public virtual DateTimeOffset CreatedAt { get; set; } + + public virtual string CreatedBy { get; set; } + + public virtual int Version { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs new file mode 100644 index 00000000..bcdde865 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs @@ -0,0 +1,20 @@ +namespace Lab.ChangeTracking.Domain; + +public record IdentityEntity +{ + public Guid Employee_Id { get; set; } + + public string Account { get; set; } + + public string Password { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string? ModifiedBy { get; set; } + + public virtual string Remark { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs new file mode 100644 index 00000000..e251d05f --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs @@ -0,0 +1 @@ +namespace Lab.ChangeTracking.Domain; diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs new file mode 100644 index 00000000..b0478a4f --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs @@ -0,0 +1,41 @@ +using ChangeTracking; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; + +namespace Lab.ChangeTracking.Domain; + +public class EmployeeRepository +{ + private readonly IDbContextFactory _employeeDbContextFactory; + + public EmployeeRepository(IDbContextFactory memberContextFactory) + { + this._employeeDbContextFactory = memberContextFactory; + } + + public async Task AddAsync(EmployeeEntity source, + CancellationToken cancel = default) + { + var dbContext = await this._employeeDbContextFactory.CreateDbContextAsync(cancel); + var srcEmployee = source.CastToIChangeTrackable(); + + return await dbContext.SaveChangesAsync(cancel); + } + + public async Task SaveChangesAsync(EmployeeEntity source, + CancellationToken cancel = default) + { + var dbContext = await this._employeeDbContextFactory.CreateDbContextAsync(cancel); + var employeeTrackable = source.CastToIChangeTrackable(); + + return await dbContext.SaveChangesAsync(cancel); + } + + public async Task InsertEmployeeAsync(EmployeeEntity srcEmployee, CancellationToken cancel = default) + { + var destEmployee = srcEmployee.To(); + var dbContext = await this._employeeDbContextFactory.CreateDbContextAsync(cancel); + await dbContext.AddAsync(destEmployee, cancel); + return await dbContext.SaveChangesAsync(cancel); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs new file mode 100644 index 00000000..0035d132 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs @@ -0,0 +1,8 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IEmployeeRepository +{ + Task SaveChangeAsync(EmployeeEntity employee, + IEnumerable excludeProperties = null, + CancellationToken cancel = default); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/TypeConverterExtensions.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/TypeConverterExtensions.cs new file mode 100644 index 00000000..0b1e88be --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/TypeConverterExtensions.cs @@ -0,0 +1,145 @@ +// using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +// +// namespace Lab.ChangeTracking.Domain; +// +// public static class TypeConverterExtensions +// { +// public static Employee To(this EmployeeValueObject srcEmployee) +// { +// if (srcEmployee == null) +// { +// return null; +// } +// +// return new Employee +// { +// Id = srcEmployee.Id, +// Name = srcEmployee.Name, +// Age = srcEmployee.Age, +// Version = srcEmployee.Version, +// Remark = srcEmployee.Remark, +// Addresses = srcEmployee.Addresses != null ? srcEmployee.Addresses.To().ToList() : null, +// Identity = srcEmployee.Identity.To(), +// CreatedAt = srcEmployee.CreatedAt, +// CreatedBy = srcEmployee.CreatedBy, +// ModifiedAt = srcEmployee.ModifiedAt, +// ModifiedBy = srcEmployee.ModifiedBy +// }; +// } +// +// public static EmployeeValueObject To(this Employee srcEmployee) +// { +// if (srcEmployee == null) +// { +// return null; +// } +// +// return new EmployeeValueObject +// { +// Id = srcEmployee.Id, +// Name = srcEmployee.Name, +// Age = srcEmployee.Age, +// Version = srcEmployee.Version, +// Remark = srcEmployee.Remark, +// Addresses = srcEmployee.Addresses.To()?.ToList(), +// Identity = srcEmployee.Identity.To(), +// CreatedAt = srcEmployee.CreatedAt, +// CreatedBy = srcEmployee.CreatedBy, +// ModifiedAt = srcEmployee.ModifiedAt, +// ModifiedBy = srcEmployee.ModifiedBy +// }; +// } +// +// public static Identity To(this IdentityEntity srcIdentity) +// { +// if (srcIdentity == null) +// { +// return null; +// } +// +// return new Identity +// { +// Employee_Id = srcIdentity.Employee_Id, +// Account = srcIdentity.Account, +// Password = srcIdentity.Password, +// Remark = srcIdentity.Remark, +// CreatedAt = srcIdentity.CreatedAt, +// CreatedBy = srcIdentity.CreatedBy, +// ModifiedAt = srcIdentity.ModifiedAt, +// ModifiedBy = srcIdentity.ModifiedBy +// }; +// } +// +// public static IdentityEntity To(this Identity srcIdentity) +// { +// if (srcIdentity == null) +// { +// return null; +// } +// +// return new IdentityEntity +// { +// Employee_Id = srcIdentity.Employee_Id, +// Account = srcIdentity.Account, +// Password = srcIdentity.Password, +// Remark = srcIdentity.Remark, +// CreatedAt = srcIdentity.CreatedAt, +// CreatedBy = srcIdentity.CreatedBy, +// ModifiedAt = srcIdentity.ModifiedAt, +// ModifiedBy = srcIdentity.ModifiedBy +// }; +// } +// +// public static Address To(this AddressEntity srcAddress) +// { +// if (srcAddress == null) +// { +// return null; +// } +// +// return new Address +// { +// Id = srcAddress.Id, +// Employee_Id = srcAddress.Employee_Id, +// Country = srcAddress.Country, +// Street = srcAddress.Street, +// CreatedAt = srcAddress.CreatedAt, +// CreatedBy = srcAddress.CreatedBy, +// ModifiedAt = srcAddress.ModifiedAt, +// ModifiedBy = srcAddress.ModifiedBy, +// Remark = srcAddress.Remark +// }; +// } +// +// public static AddressEntity To(this Address srcAddress) +// { +// if (srcAddress == null) +// { +// return null; +// } +// +// return new AddressEntity +// { +// Id = srcAddress.Id, +// Employee_Id = srcAddress.Employee_Id, +// Country = srcAddress.Country, +// Street = srcAddress.Street, +// CreatedAt = srcAddress.CreatedAt, +// CreatedBy = srcAddress.CreatedBy, +// ModifiedAt = srcAddress.ModifiedAt, +// ModifiedBy = srcAddress.ModifiedBy, +// Remark = srcAddress.Remark +// }; +// } +// +// public static IEnumerable
To(this IEnumerable srcProfiles) +// { +// return srcProfiles?.Select(p => p?.To()); +// } +// +// public static IEnumerable To(this IEnumerable
srcProfiles) +// { +// return srcProfiles?.Select(p => p?.To()); +// } +// } + diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EntityBase.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EntityBase.cs new file mode 100644 index 00000000..96bae77a --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/EntityBase.cs @@ -0,0 +1,140 @@ +using System.Collections; + +namespace Lab.ChangeTracking.Domain; + +public abstract record EntityBase : IChangeTrackable +{ + // public Guid Id + // { + // get => this._id; + // init => this._id = value; + // } + + protected readonly Dictionary _changedProperties = new(); + protected readonly Dictionary _originalValues = new(); + protected CommitState _commitState; + protected ChangeState _entityState; + protected Guid _id; + + public EntityBase AsTrackable() + { + this.Validate(); + + // this._entityState = EntityState.Added; + // this._commitState = CommitState.Unchanged; + // this._version = 1; + var properties = this.GetType().GetProperties(); + foreach (var property in properties) + { + this._originalValues.Add(property.Name, property.GetValue(this)); + } + + return this; + } + + public (Error err, bool changed) AcceptChanges(ISystemClock systemClock, + IAccessContext accessContext, + IUUIdProvider idProvider) + { + this.Validate(); + + this._commitState = CommitState.Accepted; + var (now, accessUserId) = (systemClock.GetNow(), accessContext.GetUserId()); + + + if (this.EntityState == ChangeState.Added) + { + this._id = idProvider.GenerateId(); + this.CreatedAt = now; + this.CreatedBy = accessUserId; + this.Version = 1; + } + else + { + this.Version = this.Version++; + } + + this.ModifiedAt = now; + this.ModifiedBy = accessUserId; + + // this._entityState = EntityState.Submitted; + + return (null, true); + } + + public abstract void RejectChanges(); + + public Dictionary GetChangedProperties() + { + return this._changedProperties; + } + + public ChangeState EntityState + { + get => this._entityState; + init => this._entityState = value; + } + + public CommitState CommitState + { + get => this._commitState; + init => this._commitState = value; + } + + public int Version { get; set; } + + public Dictionary GetOriginalValues() + { + return this._originalValues; + } + + public Guid Id { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string? CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string? ModifiedBy { get; set; } + + public void ChangeTrack(string propertyName, object value) + { + this.Validate(); + + var changes = this._changedProperties; + var originals = this._originalValues; + if (originals.Count <= 0) + { + throw new Exception("尚未啟用追蹤"); + } + + if (changes.ContainsKey(propertyName) == false) + { + if (originals[propertyName] != value) + { + changes.Add(propertyName, value); + this._entityState = ChangeState.Modified; + } + } + else + { + if (originals[propertyName].ToString() == value.ToString()) + { + changes.Remove(propertyName); + } + } + + if (changes.Count <= 0) + { + } + } + + private void Validate() + { + if (this.CommitState == CommitState.Accepted) + { + throw new Exception("已經同意,無法再進行修改"); + } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/Error.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/Error.cs new file mode 100644 index 00000000..aa8f1b7a --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/Error.cs @@ -0,0 +1,3 @@ +namespace Lab.ChangeTracking.Domain; + +public record Error(T Code, object Message); \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeContent.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeContent.cs new file mode 100644 index 00000000..6d899ee9 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeContent.cs @@ -0,0 +1,8 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IChangeContent +{ + Dictionary GetChangedProperties(); + + Dictionary GetOriginalValues(); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeState.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeState.cs new file mode 100644 index 00000000..d3223700 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeState.cs @@ -0,0 +1,10 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IChangeState +{ + ChangeState EntityState { get; init; } + + CommitState CommitState { get; init; } + + int Version { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeTime.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeTime.cs new file mode 100644 index 00000000..c987e109 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeTime.cs @@ -0,0 +1,16 @@ +using System.ComponentModel; + +namespace Lab.ChangeTracking.Domain; + +public interface IChangeTime +{ + public Guid Id { get; set; } + + DateTimeOffset CreatedAt { get; set; } + + string CreatedBy { get; set; } + + DateTimeOffset? ModifiedAt { get; set; } + + string? ModifiedBy { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeTrackable.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeTrackable.cs new file mode 100644 index 00000000..9708f0ea --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IChangeTrackable.cs @@ -0,0 +1,12 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IChangeTrackable : IChangeTime, IChangeState +{ + public (Error err, bool changed) AcceptChanges(ISystemClock systemClock, + IAccessContext accessContext, + IUUIdProvider idProvider); + + void ChangeTrack(string propertyName, object value); + + void RejectChanges(); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/ISystemClock.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/ISystemClock.cs new file mode 100644 index 00000000..50076e0d --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/ISystemClock.cs @@ -0,0 +1,14 @@ +namespace Lab.ChangeTracking.Domain; + +public interface ISystemClock +{ + DateTimeOffset GetNow(); +} + +public class SystemClock : ISystemClock +{ + public DateTimeOffset GetNow() + { + return DateTimeOffset.Now; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IUUIdProvider.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IUUIdProvider.cs new file mode 100644 index 00000000..a58ce071 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/IUUIdProvider.cs @@ -0,0 +1,14 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IUUIdProvider +{ + Guid GenerateId(); +} + +public class UUIdProvider : IUUIdProvider +{ + public Guid GenerateId() + { + return Guid.NewGuid(); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj new file mode 100644 index 00000000..4249eea0 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/TypeConverterExtension.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/TypeConverterExtension.cs new file mode 100644 index 00000000..04617675 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Domain/TypeConverterExtension.cs @@ -0,0 +1,114 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; + +namespace Lab.ChangeTracking.Domain; + +public static class TypeConverterExtension +{ + public static EmployeeEntity To(this Employee source) + { + return new EmployeeEntity + { + Id = source.Id, + Version = source.Version, + Name = source.Name, + Age = source.Age, + Addresses = source.Addresses.To().ToList(), + Identity = source.Identity?.To(), + CreatedAt = source.CreatedAt, + CreatedBy = source.CreatedBy, + ModifiedAt = source.ModifiedAt, + ModifiedBy = source.ModifiedBy, + Remark = source.Remark + }; + } + + public static Employee To(this EmployeeEntity source) + { + return new Employee + { + Id = source.Id, + Version = source.Version, + Name = source.Name, + Age = source.Age, + Addresses = source.Addresses.To().ToList(), + Identity = source.Identity?.To(), + CreatedAt = source.CreatedAt, + CreatedBy = source.CreatedBy, + ModifiedAt = source.ModifiedAt, + ModifiedBy = source.ModifiedBy, + Remark = source.Remark + }; + } + + public static IdentityEntity To(this Identity source) + { + return new IdentityEntity + { + Employee_Id = source.Employee_Id, + Account = source.Account, + Password = source.Password, + Remark = source.Remark, + CreatedAt = source.CreatedAt, + CreatedBy = source.CreatedBy, + ModifiedAt = source.ModifiedAt, + ModifiedBy = source.ModifiedBy + }; + } + + public static Identity To(this IdentityEntity source) + { + return new Identity + { + Employee_Id = source.Employee_Id, + Account = source.Account, + Password = source.Password, + Remark = source.Remark, + CreatedAt = source.CreatedAt, + CreatedBy = source.CreatedBy, + ModifiedAt = source.ModifiedAt, + ModifiedBy = source.ModifiedBy + }; + } + + public static Address To(this AddressEntity source) + { + return new Address + { + Id = source.Id, + Employee_Id = source.Employee_Id, + Country = source.Country, + Street = source.Street, + CreatedAt = source.CreatedAt, + CreatedBy = source.CreatedBy, + ModifiedAt = source.ModifiedAt, + ModifiedBy = source.ModifiedBy, + Remark = source.Remark, + }; + } + + public static AddressEntity To(this Address source) + { + return new AddressEntity + { + Id = source.Id, + Employee_Id = source.Employee_Id, + Country = source.Country, + Street = source.Street, + CreatedAt = source.CreatedAt, + CreatedBy = source.CreatedBy, + ModifiedAt = source.ModifiedAt, + ModifiedBy = source.ModifiedBy, + Remark = source.Remark, + }; + } + + public static IEnumerable To(this IEnumerable
sources) + { + return sources.Select(p => p.To()); + } + + public static IEnumerable
To(this IEnumerable sources) + { + return sources.Select(p => p.To()); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs new file mode 100644 index 00000000..332cf598 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs @@ -0,0 +1,48 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.ChangeTracking.Infrastructure.DB; + +public static class AppDependencyInjectionExtensions +{ + public static void AddAppEnvironment(this IServiceCollection services) + { + services.AddLogging(builder => { builder.AddConsole(); }); + services.AddSingleton(); + } + + public static void AddEntityFramework(this IServiceCollection services) + { + services.AddPooledDbContextFactory((provider, optionsBuilder) => + { + var option = provider.GetService(); + var connectionString = option.EmployeeDbConnectionString; + var loggerFactory = provider.GetService(); + optionsBuilder.UseSqlServer(connectionString) + .UseLoggerFactory(loggerFactory) + .LogTo(Console.WriteLine) + .EnableSensitiveDataLogging() + ; + }); + + // services.AddPooledDbContextFactory((provider, options) => + // { + // var option = provider.GetService(); + // var loggerFactory = provider.GetService(); + // options.UseNpgsql( + // option.MemberDbConnectionString, //只會呼叫一次 + // builder => + // builder.EnableRetryOnFailure( + // 10, + // TimeSpan.FromSeconds(30), + // new List { "57P01" })) + // + // // .UseLazyLoadingProxies() + // .UseSnakeCaseNamingConvention() + // .UseLoggerFactory(loggerFactory) + // ; + // }); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs new file mode 100644 index 00000000..d93a85cc --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs @@ -0,0 +1,32 @@ +namespace Lab.ChangeTracking.Infrastructure.DB; + +public class AppEnvironmentOption +{ + public string EmployeeDbConnectionString + { + get + { + if (string.IsNullOrWhiteSpace(this._employeeDbConnectionString)) + { + this._employeeDbConnectionString = + EnvironmentAssistant.GetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR); + } + + return this._employeeDbConnectionString; + } + set + { + this._employeeDbConnectionString = value; + Environment.SetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR, value); + } + } + + private readonly string EMPLOYEE_DB_CONN_STR = "EMPLOYEE_DB_CONNECTION_STR"; + + private string _employeeDbConnectionString; + + public void Initial() + { + var memberDbConnectionString = this.EmployeeDbConnectionString; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Address.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Address.cs new file mode 100644 index 00000000..cdddbfa0 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Address.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel; + +public class Address +{ + public Guid Id { get; set; } + + public Guid Employee_Id { get; set; } + + public Employee Employee { get; set; } + + public string Country { get; set; } + + public string Street { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string ModifiedBy { get; set; } + + public string Remark { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int SequenceId { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs new file mode 100644 index 00000000..15e8c62b --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class Employee + { + public Guid Id { get; set; } + + public int Version { get; set; } + + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + public IList
Addresses { get; set; } + + public Identity Identity { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string ModifiedBy { get; set; } + + public string Remark { get; set; } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..9ecfbc28 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,120 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = { false }; + + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identity { get; set; } + + public virtual DbSet
Addresses { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + // 改用 CI 執行 Migrate + + // if (s_migrated[0]) + // { + // return; + // } + // + // lock (s_migrated) + // { + // if (s_migrated[0] == false) + // { + // var sqlOptions = options.FindExtension(); + // if (sqlOptions != null) + // { + // this.Database.Migrate(); + // } + // + // s_migrated[0] = true; + // } + // } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new EmployeeConfiguration()); + modelBuilder.ApplyConfiguration(new IdentityConfiguration()); + modelBuilder.ApplyConfiguration(new AddressConfiguration()); + } + + internal class EmployeeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Employee"); + builder.HasKey(p => p.Id) + .IsClustered(false); + + builder.Property(p => p.Name).IsRequired(); + builder.Property(p => p.CreatedBy).IsRequired(); + builder.Property(p => p.CreatedAt).IsRequired(); + builder.Property(p => p.ModifiedAt).IsRequired(false); + builder.Property(p => p.ModifiedBy).IsRequired(false); + builder.Property(p => p.Remark).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + + private class IdentityConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Identity"); + builder.HasKey(p => p.Employee_Id).IsClustered(false); + builder.HasOne(e => e.Employee) + .WithOne(p => p.Identity) + .HasForeignKey(p => p.Employee_Id) + .OnDelete(DeleteBehavior.Cascade) + ; + + builder.Property(p => p.Account).IsRequired(); + builder.Property(p => p.Password).IsRequired(); + builder.Property(p => p.CreatedBy).IsRequired(); + builder.Property(p => p.CreatedAt).IsRequired(); + builder.Property(p => p.ModifiedAt).IsRequired(false); + builder.Property(p => p.ModifiedBy).IsRequired(false); + builder.Property(p => p.Remark).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + + private class AddressConfiguration : IEntityTypeConfiguration
+ { + public void Configure(EntityTypeBuilder
builder) + { + builder.ToTable("Address"); + builder.HasKey(p => p.Id).IsClustered(false); + builder.HasOne(e => e.Employee) + .WithMany(p => p.Addresses) + .HasForeignKey(p => p.Employee_Id) + .OnDelete(DeleteBehavior.Cascade); + + builder.Property(p => p.Country).IsRequired(); + builder.Property(p => p.Street).IsRequired(); + builder.Property(p => p.CreatedBy).IsRequired(); + builder.Property(p => p.CreatedAt).IsRequired(); + builder.Property(p => p.ModifiedBy).IsRequired(false); + builder.Property(p => p.ModifiedAt).IsRequired(false); + builder.Property(p => p.Remark).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs new file mode 100644 index 00000000..e9e2f6d9 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class Identity + { + public Guid Employee_Id { get; set; } + + // [ForeignKey("Employee_Id")] + public virtual Employee Employee { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Remark { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string? ModifiedBy { get; set; } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs new file mode 100644 index 00000000..b115d8a2 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs @@ -0,0 +1,15 @@ +namespace Lab.ChangeTracking.Infrastructure.DB; + +public class EnvironmentAssistant +{ + public static string GetEnvironmentVariable(string key) + { + var result = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrWhiteSpace(result)) + { + throw new Exception($"the key '{key}' not exist in environment variable"); + } + + return result; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj new file mode 100644 index 00000000..3a36e2d1 --- /dev/null +++ b/Property Change Tracking/ChangeTrackVersion/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + enable + enable + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTracking/ChangeTracking.sln b/Property Change Tracking/ChangeTracking/ChangeTracking.sln new file mode 100644 index 00000000..1c868fe3 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/ChangeTracking.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{1F776B7D-C182-4DC3-BA57-844657BD9AC0}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + Makefile = Makefile + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{483A9EB7-C4AF-4D68-80ED-49277985C760}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Infrastructure.DB", "src\Lab.ChangeTracking.Infrastructure.DB\Lab.ChangeTracking.Infrastructure.DB.csproj", "{A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Domain", "src\Lab.ChangeTracking.Domain\Lab.ChangeTracking.Domain.csproj", "{A1C827F2-683E-470C-A3DE-BD6DD2BE5198}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Domain.UnitTest", "src\Lab.ChangeTracking.Domain.UnitTest\Lab.ChangeTracking.Domain.UnitTest.csproj", "{7066EE2C-28A8-4408-B212-E582608AB967}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Release|Any CPU.Build.0 = Release|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Release|Any CPU.Build.0 = Release|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {7066EE2C-28A8-4408-B212-E582608AB967} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + EndGlobalSection +EndGlobal diff --git a/Property Change Tracking/ChangeTracking/Makefile b/Property Change Tracking/ChangeTracking/Makefile new file mode 100644 index 00000000..cf17f07f --- /dev/null +++ b/Property Change Tracking/ChangeTracking/Makefile @@ -0,0 +1,4 @@ +G1: + echo 'Hello World' +G2: + cmd \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/docker-compose.yml b/Property Change Tracking/ChangeTracking/docker-compose.yml new file mode 100644 index 00000000..8ecb1567 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.8" + +services: + db-sql: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=pass@w0rd1~ + ports: + - 1433:1433 \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs new file mode 100644 index 00000000..50807b93 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Unicode; +using ChangeTracking; +using Lab.ChangeTracking.Domain.EmployeeAggregate; +using Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Lab.MultiTestCase.UnitTest; +using Microsoft.EntityFrameworkCore; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.ChangeTracking.Domain.UnitTest; + +[TestClass] +public class ChangeTrackingUnitTest +{ + private readonly IEmployeeAggregate _employeeAggregate = TestAssistants.EmployeeAggregate; + + private readonly IDbContextFactory _employeeDbContextFactory = + TestAssistants.EmployeeDbContextFactory; + + [TestMethod] + public void 原本用法() + { + var source = new EmployeeEntity + { + Id = Guid.NewGuid(), + Name = "yao", + Age = 12, + }; + source.Age = 18; + Assert.AreEqual(18, source.Age); + } + + [TestMethod] + public void 追蹤() + { + var source = new EmployeeEntity + { + Id = Guid.NewGuid(), + Name = "yao", + Age = 12, + Identity = new IdentityEntity { Account = "G1234" }, + }; + var trackable = source.AsTrackable(); + trackable.Name = "小章"; + var employTrackable = trackable.CastToIChangeTrackable(); + + var employeeChangedProperties = employTrackable.ChangedProperties; + + Console.WriteLine($"{nameof(this.追蹤)}:追蹤 Employee 欄位"); + Console.WriteLine(ToJson(employeeChangedProperties)); + } + + [TestMethod] + public void 追蹤集合() + { + var source = new EmployeeEntity + { + Id = Guid.NewGuid(), + Name = "yao", + Age = 12, + Identity = new IdentityEntity { Account = "G1234" }, + Profiles = new List + { + new() { FirstName = "第一筆" }, + new() { FirstName = "將被刪掉" }, + } + }; + var trackable = source.AsTrackable(); + trackable.Profiles[0].FirstName = "變更"; + trackable.Profiles.Add(new ProfileEntity { FirstName = "新增" }); + trackable.Profiles.RemoveAt(1); + + var profileTrackable = trackable.Profiles.CastToIChangeTrackableCollection(); + + var unchangedItems = profileTrackable.UnchangedItems; + var addedItems = profileTrackable.AddedItems; + var changedItems = profileTrackable.ChangedItems; + var deleteItems = profileTrackable.DeletedItems; + + Console.WriteLine($"{nameof(this.追蹤集合)}:追蹤 Profiles 集合"); + Console.WriteLine($"UnchangedItems:{ToJson(unchangedItems)}"); + Console.WriteLine($"AddItem:{ToJson(addedItems)}"); + Console.WriteLine($"ChangedItems:{ToJson(changedItems)}"); + Console.WriteLine($"DeleteItems:{ToJson(deleteItems)}"); + Console.WriteLine($"{nameof(this.追蹤集合)}:追蹤 Profiles[0] 變更屬性"); + var changeTrackable = trackable.Profiles[0].CastToIChangeTrackable(); + Console.WriteLine($"Profiles[0] 變更欄位:{ToJson(changeTrackable.ChangedProperties)}"); + } + + [TestMethod] + public void 追蹤複雜型別() + { + var source = new EmployeeEntity + { + Id = Guid.NewGuid(), + Name = "yao", + Age = 12, + Identity = new IdentityEntity { Account = "G1234" }, + }; + var trackable = source.AsTrackable(); + trackable.Name = "小章"; + trackable.Identity.Account = "yao"; + var employTrackable = trackable.CastToIChangeTrackable(); + var identityTrackable = trackable.Identity.CastToIChangeTrackable(); + + var employeeChangedProperties = employTrackable.ChangedProperties; + var identityChangedProperties = identityTrackable.ChangedProperties; + + Console.WriteLine($"{nameof(this.追蹤複雜型別)}:追蹤 Employee 欄位"); + Console.WriteLine(ToJson(employeeChangedProperties)); + Console.WriteLine($"{nameof(this.追蹤複雜型別)}:追蹤 Identity 欄位"); + Console.WriteLine(ToJson(identityChangedProperties)); + } + + [TestMethod] + public void 異動追蹤後存檔() + { + var toDB = Insert(); + var source = new EmployeeEntity + { + Id = toDB.Id, + Name = "yao", + Age = 12, + Identity = new IdentityEntity(), + }; + var employeeEntity = this._employeeAggregate.ModifyFlowAsync(source).Result; + this.DataShouldOk(source); + } + + private void DataShouldOk(EmployeeEntity source) + { + var dbContext = this._employeeDbContextFactory.CreateDbContext(); + var actual = dbContext.Employees + .Where(p => p.Id == source.Id) + .Include(p => p.Identity) + .First() + ; + + Assert.AreEqual("小章", actual.Name); + Assert.AreEqual("9527", actual.Identity.Password); + } + + private static Employee Insert() + { + using var dbContext = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + var toDB = new Employee + { + Id = Guid.NewGuid(), + Age = 18, + Name = "yao", + CreateAt = DateTimeOffset.Now, + CreateBy = "TEST", + Identity = new Identity + { + Account = "yao", + Password = "123456", + CreateAt = DateTimeOffset.Now, + CreateBy = "TEST", + } + }; + dbContext.Employees.Add(toDB); + dbContext.SaveChanges(); + return toDB; + } + + private static string ToJson(T instance) + { + var serialize = JsonSerializer.Serialize(instance, + new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.Create( + UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs) + }); + return serialize; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj new file mode 100644 index 00000000..72a505c5 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs new file mode 100644 index 00000000..ca75b47b --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs @@ -0,0 +1,31 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.MultiTestCase.UnitTest; + +[TestClass] +public class MsTestHook +{ + [AssemblyCleanup] + public static void Cleanup() + { + TestAssistants.SetTestEnvironmentVariable(); + var db = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + } + + [AssemblyInitialize] + public static void Setup(TestContext context) + { + TestAssistants.SetTestEnvironmentVariable(); + var db = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + + db.Database.EnsureCreated(); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs new file mode 100644 index 00000000..68fe6717 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs @@ -0,0 +1,48 @@ +using System; +using Lab.ChangeTracking.Domain.EmployeeAggregate; +using Lab.ChangeTracking.Domain.EmployeeAggregate.Repository; +using Lab.ChangeTracking.Infrastructure.DB; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Lab.MultiTestCase.UnitTest; + +// assistant +internal class TestAssistants +{ + private static IServiceProvider _serviceProvider; + + public static IDbContextFactory EmployeeDbContextFactory => + _serviceProvider.GetService>(); + + public static IEmployeeRepository EmployeeRepository => + _serviceProvider.GetService(); + + public static IEmployeeAggregate EmployeeAggregate => + _serviceProvider.GetService(); + + static TestAssistants() + { + var services = new ServiceCollection(); + ConfigureTestServices(services); + SetTestEnvironmentVariable(); + } + + public static void ConfigureTestServices(IServiceCollection services) + { + services.AddAppEnvironment(); + services.AddEntityFramework(); + + services.AddSingleton(); + services.AddSingleton(); + _serviceProvider = services.BuildServiceProvider(); + } + + public static void SetTestEnvironmentVariable() + { + var option = _serviceProvider.GetService(); + option.EmployeeDbConnectionString = + "Data Source=localhost;Initial Catalog=EmployeeDb;Integrated Security=false;User ID=sa;Password=pass@w0rd1~;MultipleActiveResultSets=True;TrustServerCertificate=True"; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/Annotations.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/Annotations.cs new file mode 100644 index 00000000..c93bbecf --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/Annotations.cs @@ -0,0 +1,1603 @@ +/* MIT License + +Copyright (c) 2016 JetBrains http://www.jetbrains.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +using System; +// ReSharper disable UnusedType.Global + +#pragma warning disable 1591 +// ReSharper disable UnusedMember.Global +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable IntroduceOptionalParameters.Global +// ReSharper disable MemberCanBeProtected.Global +// ReSharper disable InconsistentNaming + +namespace Lab.ChangeTracking.Domain.Annotations +{ + /// + /// Indicates that the value of the marked element could be null sometimes, + /// so checking for null is required before its usage. + /// + /// + /// [CanBeNull] object Test() => null; + /// + /// void UseTest() { + /// var p = Test(); + /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class CanBeNullAttribute : Attribute { } + + /// + /// Indicates that the value of the marked element can never be null. + /// + /// + /// [NotNull] object Foo() { + /// return null; // Warning: Possible 'null' assignment + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class NotNullAttribute : Attribute { } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can never be null. + /// + /// + /// public void Foo([ItemNotNull]List<string> books) + /// { + /// foreach (var book in books) { + /// if (book != null) // Warning: Expression is always true + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemNotNullAttribute : Attribute { } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can be null. + /// + /// + /// public void Foo([ItemCanBeNull]List<string> books) + /// { + /// foreach (var book in books) + /// { + /// // Warning: Possible 'System.NullReferenceException' + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemCanBeNullAttribute : Attribute { } + + /// + /// Indicates that the marked method builds string by the format pattern and (optional) arguments. + /// The parameter, which contains the format string, should be given in the constructor. The format string + /// should be in -like form. + /// + /// + /// [StringFormatMethod("message")] + /// void ShowError(string message, params object[] args) { /* do something */ } + /// + /// void Foo() { + /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string + /// } + /// + [AttributeUsage( + AttributeTargets.Constructor | AttributeTargets.Method | + AttributeTargets.Property | AttributeTargets.Delegate)] + public sealed class StringFormatMethodAttribute : Attribute + { + /// + /// Specifies which parameter of an annotated method should be treated as the format string + /// + public StringFormatMethodAttribute([NotNull] string formatParameterName) + { + FormatParameterName = formatParameterName; + } + + [NotNull] public string FormatParameterName { get; } + } + + /// + /// Indicates that the marked parameter is a message template where placeholders are to be replaced by the following arguments + /// in the order in which they appear + /// + /// + /// void LogInfo([StructuredMessageTemplate]string message, params object[] args) { /* do something */ } + /// + /// void Foo() { + /// LogInfo("User created: {username}"); // Warning: Non-existing argument in format string + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class StructuredMessageTemplateAttribute : Attribute {} + + /// + /// Use this annotation to specify a type that contains static or const fields + /// with values for the annotated property/field/parameter. + /// The specified type will be used to improve completion suggestions. + /// + /// + /// namespace TestNamespace + /// { + /// public class Constants + /// { + /// public static int INT_CONST = 1; + /// public const string STRING_CONST = "1"; + /// } + /// + /// public class Class1 + /// { + /// [ValueProvider("TestNamespace.Constants")] public int myField; + /// public void Foo([ValueProvider("TestNamespace.Constants")] string str) { } + /// + /// public void Test() + /// { + /// Foo(/*try completion here*/);// + /// myField = /*try completion here*/ + /// } + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field, + AllowMultiple = true)] + public sealed class ValueProviderAttribute : Attribute + { + public ValueProviderAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + /// + /// Indicates that the integral value falls into the specified interval. + /// It's allowed to specify multiple non-intersecting intervals. + /// Values of interval boundaries are inclusive. + /// + /// + /// void Foo([ValueRange(0, 100)] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate, + AllowMultiple = true)] + public sealed class ValueRangeAttribute : Attribute + { + public object From { get; } + public object To { get; } + + public ValueRangeAttribute(long from, long to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(ulong from, ulong to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(long value) + { + From = To = value; + } + + public ValueRangeAttribute(ulong value) + { + From = To = value; + } + } + + /// + /// Indicates that the integral value never falls below zero. + /// + /// + /// void Foo([NonNegativeValue] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate)] + public sealed class NonNegativeValueAttribute : Attribute { } + + /// + /// Indicates that the function argument should be a string literal and match one + /// of the parameters of the caller function. For example, ReSharper annotates + /// the parameter of . + /// + /// + /// void Foo(string param) { + /// if (param == null) + /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InvokerParameterNameAttribute : Attribute { } + + /// + /// Indicates that the method is contained in a type that implements + /// System.ComponentModel.INotifyPropertyChanged interface and this method + /// is used to notify that some property value changed. + /// + /// + /// The method should be non-static and conform to one of the supported signatures: + /// + /// NotifyChanged(string) + /// NotifyChanged(params string[]) + /// NotifyChanged{T}(Expression{Func{T}}) + /// NotifyChanged{T,U}(Expression{Func{T,U}}) + /// SetProperty{T}(ref T, T, string) + /// + /// + /// + /// public class Foo : INotifyPropertyChanged { + /// public event PropertyChangedEventHandler PropertyChanged; + /// + /// [NotifyPropertyChangedInvocator] + /// protected virtual void NotifyChanged(string propertyName) { ... } + /// + /// string _name; + /// + /// public string Name { + /// get { return _name; } + /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } + /// } + /// } + /// + /// Examples of generated notifications: + /// + /// NotifyChanged("Property") + /// NotifyChanged(() => Property) + /// NotifyChanged((VM x) => x.Property) + /// SetProperty(ref myField, value, "Property") + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute + { + public NotifyPropertyChangedInvocatorAttribute() { } + public NotifyPropertyChangedInvocatorAttribute([NotNull] string parameterName) + { + ParameterName = parameterName; + } + + [CanBeNull] public string ParameterName { get; } + } + + /// + /// Describes dependency between method input and output. + /// + /// + ///

Function Definition Table syntax:

+ /// + /// FDT ::= FDTRow [;FDTRow]* + /// FDTRow ::= Input => Output | Output <= Input + /// Input ::= ParameterName: Value [, Input]* + /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} + /// Value ::= true | false | null | notnull | canbenull + /// + /// If the method has a single input parameter, its name could be omitted.
+ /// Using halt (or void/nothing, which is the same) for the method output + /// means that the method doesn't return normally (throws or terminates the process).
+ /// Value canbenull is only applicable for output parameters.
+ /// You can use multiple [ContractAnnotation] for each FDT row, or use single attribute + /// with rows separated by the semicolon. There is no notion of order rows, all rows are checked + /// for applicability and applied per each program state tracked by the analysis engine.
+ ///
+ /// + /// + /// [ContractAnnotation("=> halt")] + /// public void TerminationMethod() + /// + /// + /// [ContractAnnotation("null <= param:null")] // reverse condition syntax + /// public string GetName(string surname) + /// + /// + /// [ContractAnnotation("s:null => true")] + /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() + /// + /// + /// // A method that returns null if the parameter is null, + /// // and not null if the parameter is not null + /// [ContractAnnotation("null => null; notnull => notnull")] + /// public object Transform(object data) + /// + /// + /// [ContractAnnotation("=> true, result: notnull; => false, result: null")] + /// public bool TryParse(string s, out Person result) + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public sealed class ContractAnnotationAttribute : Attribute + { + public ContractAnnotationAttribute([NotNull] string contract) + : this(contract, false) { } + + public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) + { + Contract = contract; + ForceFullStates = forceFullStates; + } + + [NotNull] public string Contract { get; } + + public bool ForceFullStates { get; } + } + + /// + /// Indicates whether the marked element should be localized. + /// + /// + /// [LocalizationRequiredAttribute(true)] + /// class Foo { + /// string str = "my string"; // Warning: Localizable string + /// } + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class LocalizationRequiredAttribute : Attribute + { + public LocalizationRequiredAttribute() : this(true) { } + + public LocalizationRequiredAttribute(bool required) + { + Required = required; + } + + public bool Required { get; } + } + + /// + /// Indicates that the value of the marked type (or its derivatives) + /// cannot be compared using '==' or '!=' operators and Equals() + /// should be used instead. However, using '==' or '!=' for comparison + /// with null is always permitted. + /// + /// + /// [CannotApplyEqualityOperator] + /// class NoEquality { } + /// + /// class UsesNoEquality { + /// void Test() { + /// var ca1 = new NoEquality(); + /// var ca2 = new NoEquality(); + /// if (ca1 != null) { // OK + /// bool condition = ca1 == ca2; // Warning + /// } + /// } + /// } + /// + [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] + public sealed class CannotApplyEqualityOperatorAttribute : Attribute { } + + /// + /// When applied to a target attribute, specifies a requirement for any type marked + /// with the target attribute to implement or inherit specific type or types. + /// + /// + /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement + /// class ComponentAttribute : Attribute { } + /// + /// [Component] // ComponentAttribute requires implementing IComponent interface + /// class MyComponent : IComponent { } + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [BaseTypeRequired(typeof(Attribute))] + public sealed class BaseTypeRequiredAttribute : Attribute + { + public BaseTypeRequiredAttribute([NotNull] Type baseType) + { + BaseType = baseType; + } + + [NotNull] public Type BaseType { get; } + } + + /// + /// Indicates that the marked symbol is used implicitly (e.g. via reflection, in external library), + /// so this symbol will be ignored by usage-checking inspections.
+ /// You can use and + /// to configure how this attribute is applied. + ///
+ /// + /// [UsedImplicitly] + /// public class TypeConverter {} + /// + /// public class SummaryData + /// { + /// [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] + /// public SummaryData() {} + /// } + /// + /// [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors | ImplicitUseTargetFlags.Default)] + /// public interface IService {} + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class UsedImplicitlyAttribute : Attribute + { + public UsedImplicitlyAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + public ImplicitUseKindFlags UseKindFlags { get; } + + public ImplicitUseTargetFlags TargetFlags { get; } + } + + /// + /// Can be applied to attributes, type parameters, and parameters of a type assignable from . + /// When applied to an attribute, the decorated attribute behaves the same as . + /// When applied to a type parameter or to a parameter of type , + /// indicates that the corresponding type is used implicitly. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter | AttributeTargets.Parameter)] + public sealed class MeansImplicitUseAttribute : Attribute + { + public MeansImplicitUseAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; } + + [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; } + } + + /// + /// Specifies the details of implicitly used symbol when it is marked + /// with or . + /// + [Flags] + public enum ImplicitUseKindFlags + { + Default = Access | Assign | InstantiatedWithFixedConstructorSignature, + /// Only entity marked with attribute considered used. + Access = 1, + /// Indicates implicit assignment to a member. + Assign = 2, + /// + /// Indicates implicit instantiation of a type with fixed constructor signature. + /// That means any unused constructor parameters won't be reported as such. + /// + InstantiatedWithFixedConstructorSignature = 4, + /// Indicates implicit instantiation of a type. + InstantiatedNoFixedConstructorSignature = 8, + } + + /// + /// Specifies what is considered to be used implicitly when marked + /// with or . + /// + [Flags] + public enum ImplicitUseTargetFlags + { + Default = Itself, + Itself = 1, + /// Members of the type marked with the attribute are considered used. + Members = 2, + /// Inherited entities are considered used. + WithInheritors = 4, + /// Entity marked with the attribute and all its members considered used. + WithMembers = Itself | Members + } + + /// + /// This attribute is intended to mark publicly available API, + /// which should not be removed and so is treated as used. + /// + [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class PublicAPIAttribute : Attribute + { + public PublicAPIAttribute() { } + + public PublicAPIAttribute([NotNull] string comment) + { + Comment = comment; + } + + [CanBeNull] public string Comment { get; } + } + + /// + /// Tells the code analysis engine if the parameter is completely handled when the invoked method is on stack. + /// If the parameter is a delegate, indicates that delegate can only be invoked during method execution + /// (the delegate can be invoked zero or multiple times, but not stored to some field and invoked later, + /// when the containing method is no longer on the execution stack). + /// If the parameter is an enumerable, indicates that it is enumerated while the method is executed. + /// If is true, the attribute will only takes effect if the method invocation is located under the 'await' expression. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InstantHandleAttribute : Attribute + { + /// + /// Require the method invocation to be used under the 'await' expression for this attribute to take effect on code analysis engine. + /// Can be used for delegate/enumerable parameters of 'async' methods. + /// + public bool RequireAwait { get; set; } + } + + /// + /// Indicates that a method does not make any observable state changes. + /// The same as System.Diagnostics.Contracts.PureAttribute. + /// + /// + /// [Pure] int Multiply(int x, int y) => x * y; + /// + /// void M() { + /// Multiply(123, 42); // Warning: Return value of pure method is not used + /// } + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class PureAttribute : Attribute { } + + /// + /// Indicates that the return value of the method invocation must be used. + /// + /// + /// Methods decorated with this attribute (in contrast to pure methods) might change state, + /// but make no sense without using their return value.
+ /// Similarly to , this attribute + /// will help to detect usages of the method when the return value is not used. + /// Optionally, you can specify a message to use when showing warnings, e.g. + /// [MustUseReturnValue("Use the return value to...")]. + ///
+ [AttributeUsage(AttributeTargets.Method)] + public sealed class MustUseReturnValueAttribute : Attribute + { + public MustUseReturnValueAttribute() { } + + public MustUseReturnValueAttribute([NotNull] string justification) + { + Justification = justification; + } + + [CanBeNull] public string Justification { get; } + } + + /// + /// This annotation allows to enforce allocation-less usage patterns of delegates for performance-critical APIs. + /// When this annotation is applied to the parameter of delegate type, IDE checks the input argument of this parameter: + /// * When lambda expression or anonymous method is passed as an argument, IDE verifies that the passed closure + /// has no captures of the containing local variables and the compiler is able to cache the delegate instance + /// to avoid heap allocations. Otherwise the warning is produced. + /// * IDE warns when method name or local function name is passed as an argument as this always results + /// in heap allocation of the delegate instance. + /// + /// + /// In C# 9.0 code IDE would also suggest to annotate the anonymous function with 'static' modifier + /// to make use of the similar analysis provided by the language/compiler. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public class RequireStaticDelegateAttribute : Attribute + { + public bool IsError { get; set; } + } + + /// + /// Indicates the type member or parameter of some type, that should be used instead of all other ways + /// to get the value of that type. This annotation is useful when you have some "context" value evaluated + /// and stored somewhere, meaning that all other ways to get this value must be consolidated with existing one. + /// + /// + /// class Foo { + /// [ProvidesContext] IBarService _barService = ...; + /// + /// void ProcessNode(INode node) { + /// DoSomething(node, node.GetGlobalServices().Bar); + /// // ^ Warning: use value of '_barService' field + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Method | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.GenericParameter)] + public sealed class ProvidesContextAttribute : Attribute { } + + /// + /// Indicates that a parameter is a path to a file or a folder within a web project. + /// Path can be relative or absolute, starting from web root (~). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class PathReferenceAttribute : Attribute + { + public PathReferenceAttribute() { } + + public PathReferenceAttribute([NotNull, PathReference] string basePath) + { + BasePath = basePath; + } + + [CanBeNull] public string BasePath { get; } + } + + /// + /// An extension method marked with this attribute is processed by code completion + /// as a 'Source Template'. When the extension method is completed over some expression, its source code + /// is automatically expanded like a template at call site. + /// + /// + /// Template method body can contain valid source code and/or special comments starting with '$'. + /// Text inside these comments is added as source code when the template is applied. Template parameters + /// can be used either as additional method parameters or as identifiers wrapped in two '$' signs. + /// Use the attribute to specify macros for parameters. + /// + /// + /// In this example, the 'forEach' method is a source template available over all values + /// of enumerable types, producing ordinary C# 'foreach' statement and placing caret inside block: + /// + /// [SourceTemplate] + /// public static void forEach<T>(this IEnumerable<T> xs) { + /// foreach (var x in xs) { + /// //$ $END$ + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class SourceTemplateAttribute : Attribute { } + + /// + /// Allows specifying a macro for a parameter of a source template. + /// + /// + /// You can apply the attribute on the whole method or on any of its additional parameters. The macro expression + /// is defined in the property. When applied on a method, the target + /// template parameter is defined in the property. To apply the macro silently + /// for the parameter, set the property value = -1. + /// + /// + /// Applying the attribute on a source template method: + /// + /// [SourceTemplate, Macro(Target = "item", Expression = "suggestVariableName()")] + /// public static void forEach<T>(this IEnumerable<T> collection) { + /// foreach (var item in collection) { + /// //$ $END$ + /// } + /// } + /// + /// Applying the attribute on a template method parameter: + /// + /// [SourceTemplate] + /// public static void something(this Entity x, [Macro(Expression = "guid()", Editable = -1)] string newguid) { + /// /*$ var $x$Id = "$newguid$" + x.ToString(); + /// x.DoSomething($x$Id); */ + /// } + /// + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = true)] + public sealed class MacroAttribute : Attribute + { + /// + /// Allows specifying a macro that will be executed for a source template + /// parameter when the template is expanded. + /// + [CanBeNull] public string Expression { get; set; } + + /// + /// Allows specifying which occurrence of the target parameter becomes editable when the template is deployed. + /// + /// + /// If the target parameter is used several times in the template, only one occurrence becomes editable; + /// other occurrences are changed synchronously. To specify the zero-based index of the editable occurrence, + /// use values >= 0. To make the parameter non-editable when the template is expanded, use -1. + /// + public int Editable { get; set; } + + /// + /// Identifies the target parameter of a source template if the + /// is applied on a template method. + /// + [CanBeNull] public string Target { get; set; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute + { + public AspMvcAreaMasterLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute + { + public AspMvcAreaPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaViewLocationFormatAttribute : Attribute + { + public AspMvcAreaViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcMasterLocationFormatAttribute : Attribute + { + public AspMvcMasterLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcPartialViewLocationFormatAttribute : Attribute + { + public AspMvcPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcViewLocationFormatAttribute : Attribute + { + public AspMvcViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC action. If applied to a method, the MVC action name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcActionAttribute : Attribute + { + public AspMvcActionAttribute() { } + + public AspMvcActionAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC area. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcAreaAttribute : Attribute + { + public AspMvcAreaAttribute() { } + + public AspMvcAreaAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is + /// an MVC controller. If applied to a method, the MVC controller name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcControllerAttribute : Attribute + { + public AspMvcControllerAttribute() { } + + public AspMvcControllerAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC Master. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcMasterAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC model type. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, Object). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcModelTypeAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is an MVC + /// partial view. If applied to a method, the MVC partial view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcPartialViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Allows disabling inspections for MVC views within a class or a method. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class AspMvcSuppressViewErrorAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcDisplayTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC editor template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcEditorTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC template. + /// Use this attribute for custom wrappers similar to + /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component. If applied to a method, the MVC view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Controller.View(Object). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component name. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewComponentAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component view. If applied to a method, the MVC view component view name is default. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewComponentViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. When applied to a parameter of an attribute, + /// indicates that this parameter is an MVC action name. + /// + /// + /// [ActionName("Foo")] + /// public ActionResult Login(string returnUrl) { + /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK + /// return RedirectToAction("Bar"); // Error: Cannot resolve action + /// } + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] + public sealed class AspMvcActionSelectorAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] + public sealed class HtmlElementAttributesAttribute : Attribute + { + public HtmlElementAttributesAttribute() { } + + public HtmlElementAttributesAttribute([NotNull] string name) + { + Name = name; + } + + [CanBeNull] public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class HtmlAttributeValueAttribute : Attribute + { + public HtmlAttributeValueAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + /// + /// Razor attribute. Indicates that the marked parameter or method is a Razor section. + /// Use this attribute for custom wrappers similar to + /// System.Web.WebPages.WebPageBase.RenderSection(String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + public sealed class RazorSectionAttribute : Attribute { } + + /// + /// Indicates how method, constructor invocation, or property access + /// over collection type affects the contents of the collection. + /// When applied to a return value of a method indicates if the returned collection + /// is created exclusively for the caller (CollectionAccessType.UpdatedContent) or + /// can be read/updated from outside (CollectionAccessType.Read | CollectionAccessType.UpdatedContent) + /// Use to specify the access type. + /// + /// + /// Using this attribute only makes sense if all collection methods are marked with this attribute. + /// + /// + /// public class MyStringCollection : List<string> + /// { + /// [CollectionAccess(CollectionAccessType.Read)] + /// public string GetFirstString() + /// { + /// return this.ElementAt(0); + /// } + /// } + /// class Test + /// { + /// public void Foo() + /// { + /// // Warning: Contents of the collection is never updated + /// var col = new MyStringCollection(); + /// string x = col.GetFirstString(); + /// } + /// } + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property | AttributeTargets.ReturnValue)] + public sealed class CollectionAccessAttribute : Attribute + { + public CollectionAccessAttribute(CollectionAccessType collectionAccessType) + { + CollectionAccessType = collectionAccessType; + } + + public CollectionAccessType CollectionAccessType { get; } + } + + /// + /// Provides a value for the to define + /// how the collection method invocation affects the contents of the collection. + /// + [Flags] + public enum CollectionAccessType + { + /// Method does not use or modify content of the collection. + None = 0, + /// Method only reads content of the collection but does not modify it. + Read = 1, + /// Method can change content of the collection but does not add new elements. + ModifyExistingContent = 2, + /// Method can add new elements to the collection. + UpdatedContent = ModifyExistingContent | 4 + } + + /// + /// Indicates that the marked method is assertion method, i.e. it halts the control flow if + /// one of the conditions is satisfied. To set the condition, mark one of the parameters with + /// attribute. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class AssertionMethodAttribute : Attribute { } + + /// + /// Indicates the condition parameter of the assertion method. The method itself should be + /// marked by attribute. The mandatory argument of + /// the attribute is the assertion type. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AssertionConditionAttribute : Attribute + { + public AssertionConditionAttribute(AssertionConditionType conditionType) + { + ConditionType = conditionType; + } + + public AssertionConditionType ConditionType { get; } + } + + /// + /// Specifies assertion type. If the assertion method argument satisfies the condition, + /// then the execution continues. Otherwise, execution is assumed to be halted. + /// + public enum AssertionConditionType + { + /// Marked parameter should be evaluated to true. + IS_TRUE = 0, + /// Marked parameter should be evaluated to false. + IS_FALSE = 1, + /// Marked parameter should be evaluated to null value. + IS_NULL = 2, + /// Marked parameter should be evaluated to not null value. + IS_NOT_NULL = 3, + } + + /// + /// Indicates that the marked method unconditionally terminates control flow execution. + /// For example, it could unconditionally throw exception. + /// + [Obsolete("Use [ContractAnnotation('=> halt')] instead")] + [AttributeUsage(AttributeTargets.Method)] + public sealed class TerminatesProgramAttribute : Attribute { } + + /// + /// Indicates that the method is a pure LINQ method, with postponed enumeration (like Enumerable.Select, + /// .Where). This annotation allows inference of [InstantHandle] annotation for parameters + /// of delegate type by analyzing LINQ method chains. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class LinqTunnelAttribute : Attribute { } + + /// + /// Indicates that IEnumerable passed as a parameter is not enumerated. + /// Use this annotation to suppress the 'Possible multiple enumeration of IEnumerable' inspection. + /// + /// + /// static void ThrowIfNull<T>([NoEnumeration] T v, string n) where T : class + /// { + /// // custom check for null but no enumeration + /// } + /// + /// void Foo(IEnumerable<string> values) + /// { + /// ThrowIfNull(values, nameof(values)); + /// var x = values.ToList(); // No warnings about multiple enumeration + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class NoEnumerationAttribute : Attribute { } + + /// + /// Indicates that the marked parameter, field, or property is a regular expression pattern. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class RegexPatternAttribute : Attribute { } + + /// + /// Language of injected code fragment inside marked by string literal. + /// + public enum InjectedLanguage + { + CSS, + HTML, + JAVASCRIPT, + JSON, + XML + } + + /// + /// Indicates that the marked parameter, field, or property is accepting a string literal + /// containing code fragment in a language specified by the . + /// + /// + /// void Foo([LanguageInjection(InjectedLanguage.CSS, Prefix = "body{", Suffix = "}")] string cssProps) + /// { + /// // cssProps should only contains a list of CSS properties + /// } + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class LanguageInjectionAttribute : Attribute + { + public LanguageInjectionAttribute(InjectedLanguage injectedLanguage) + { + InjectedLanguage = injectedLanguage; + } + + /// Specify a language of injected code fragment. + public InjectedLanguage InjectedLanguage { get; } + /// Specify a string that "precedes" injected string literal. + [CanBeNull] public string Prefix { get; set; } + /// Specify a string that "follows" injected string literal. + [CanBeNull] public string Suffix { get; set; } + } + + /// + /// Prevents the Member Reordering feature from tossing members of the marked class. + /// + /// + /// The attribute must be mentioned in your member reordering patterns. + /// + [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.Enum)] + public sealed class NoReorderAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the type that has ItemsSource property and should be treated + /// as ItemsControl-derived type, to enable inner items DataContext type resolve. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class XamlItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the property of some BindingBase-derived type, that + /// is used to bind some item of ItemsControl-derived type. This annotation will + /// enable the DataContext type resolve for XAML bindings for such properties. + /// + /// + /// Property should have the tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class XamlItemBindingOfItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the property of some Style-derived type, that + /// is used to style items of ItemsControl-derived type. This annotation will + /// enable the DataContext type resolve for XAML bindings for such properties. + /// + /// + /// Property should have the tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class XamlItemStyleOfItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates that DependencyProperty has OneWay binding mode by default. + /// + /// + /// This attribute must be applied to DependencyProperty's CLR accessor property if it is present, to DependencyProperty descriptor field otherwise. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class XamlOneWayBindingModeByDefaultAttribute : Attribute { } + + /// + /// XAML attribute. Indicates that DependencyProperty has TwoWay binding mode by default. + /// + /// + /// This attribute must be applied to DependencyProperty's CLR accessor property if it is present, to DependencyProperty descriptor field otherwise. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class XamlTwoWayBindingModeByDefaultAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspChildControlTypeAttribute : Attribute + { + public AspChildControlTypeAttribute([NotNull] string tagName, [NotNull] Type controlType) + { + TagName = tagName; + ControlType = controlType; + } + + [NotNull] public string TagName { get; } + + [NotNull] public Type ControlType { get; } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldsAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspMethodPropertyAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspRequiredAttributeAttribute : Attribute + { + public AspRequiredAttributeAttribute([NotNull] string attribute) + { + Attribute = attribute; + } + + [NotNull] public string Attribute { get; } + } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspTypePropertyAttribute : Attribute + { + public bool CreateConstructorReferences { get; } + + public AspTypePropertyAttribute(bool createConstructorReferences) + { + CreateConstructorReferences = createConstructorReferences; + } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorImportNamespaceAttribute : Attribute + { + public RazorImportNamespaceAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorInjectionAttribute : Attribute + { + public RazorInjectionAttribute([NotNull] string type, [NotNull] string fieldName) + { + Type = type; + FieldName = fieldName; + } + + [NotNull] public string Type { get; } + + [NotNull] public string FieldName { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorDirectiveAttribute : Attribute + { + public RazorDirectiveAttribute([NotNull] string directive) + { + Directive = directive; + } + + [NotNull] public string Directive { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorPageBaseTypeAttribute : Attribute + { + public RazorPageBaseTypeAttribute([NotNull] string baseType) + { + BaseType = baseType; + } + public RazorPageBaseTypeAttribute([NotNull] string baseType, string pageName) + { + BaseType = baseType; + PageName = pageName; + } + + [NotNull] public string BaseType { get; } + [CanBeNull] public string PageName { get; } + } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorHelperCommonAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class RazorLayoutAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteLiteralMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class RazorWriteMethodParameterAttribute : Attribute { } + + /// + /// Indicates that the marked parameter, field, or property is a route template. + /// + /// + /// This attribute allows IDE to recognize the use of web frameworks' route templates + /// to enable syntax highlighting, code completion, navigation, rename and other features in string literals. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class RouteTemplateAttribute : Attribute { } + + /// + /// Indicates that the marked type is custom route parameter constraint, + /// which is registered in application's Startup with name ConstraintName + /// + /// + /// You can specify ProposedType if target constraint matches only route parameters of specific type, + /// it will allow IDE to create method's parameter from usage in route template + /// with specified type instead of default System.String + /// and check if constraint's proposed type conflicts with matched parameter's type + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class RouteParameterConstraintAttribute : Attribute + { + [NotNull] public string ConstraintName { get; } + [CanBeNull] public Type ProposedType { get; set; } + + public RouteParameterConstraintAttribute([NotNull] string constraintName) + { + ConstraintName = constraintName; + } + } + + /// + /// Indicates that the marked parameter, field, or property is an URI string. + /// + /// + /// This attribute enables code completion, navigation, rename and other features + /// in URI string literals assigned to annotated parameter, field or property. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class UriStringAttribute : Attribute + { + public UriStringAttribute() { } + + public UriStringAttribute(string httpVerb) + { + HttpVerb = httpVerb; + } + + [CanBeNull] public string HttpVerb { get; } + } + + /// + /// + /// Defines the code search template using the Structural Search and Replace syntax. + /// It allows you to find and, if necessary, replace blocks of code that match a specific pattern. + /// Search and replace patterns consist of a textual part and placeholders. + /// Textural part must contain only identifiers allowed in the target language and will be matched exactly (white spaces, tabulation characters, and line breaks are ignored). + /// Placeholders allow matching variable parts of the target code blocks. + /// A placeholder has the following format: $placeholder_name$- where placeholder_name is an arbitrary identifier. + /// + /// + /// Available placeholders: + /// + /// $this$ - expression of containing type + /// $thisType$ - containing type + /// $member$ - current member placeholder + /// $qualifier$ - this placeholder is available in the replace pattern and can be used to insert qualifier expression matched by the $member$ placeholder. + /// (Note that if $qualifier$ placeholder is used, then $member$ placeholder will match only qualified references) + /// $expression$ - expression of any type + /// $identifier$ - identifier placeholder + /// $args$ - any number of arguments + /// $arg$ - single argument + /// $arg1$ ... $arg10$ - single argument + /// $stmts$ - any number of statements + /// $stmt$ - single statement + /// $stmt1$ ... $stmt10$ - single statement + /// $name{Expression, 'Namespace.FooType'}$ - expression with 'Namespace.FooType' type + /// $expression{'Namespace.FooType'}$ - expression with 'Namespace.FooType' type + /// $name{Type, 'Namespace.FooType'}$ - 'Namespace.FooType' type + /// $type{'Namespace.FooType'}$ - 'Namespace.FooType' type + /// $statement{1,2}$ - 1 or 2 statements + /// + /// + /// + /// Note that you can also define your own placeholders of the supported types and specify arguments for each placeholder type. + /// This can be done using the following format: $name{type, arguments}$. Where 'name' - is the name of your placeholder, + /// 'type' - is the type of your placeholder (one of the following: Expression, Type, Identifier, Statement, Argument, Member), + /// 'arguments' - arguments list for your placeholder. Each placeholder type supports it's own arguments, check examples below for mode details. + /// Placeholder type may be omitted and determined from the placeholder name, if name has one of the following prefixes: + /// + /// expr, expression - expression placeholder, e.g. $exprPlaceholder{}$, $expressionFoo{}$ + /// arg, argument - argument placeholder, e.g. $argPlaceholder{}$, $argumentFoo{}$ + /// ident, identifier - identifier placeholder, e.g. $identPlaceholder{}$, $identifierFoo{}$ + /// stmt, statement - statement placeholder, e.g. $stmtPlaceholder{}$, $statementFoo{}$ + /// type - type placeholder, e.g. $typePlaceholder{}$, $typeFoo{}$ + /// member - member placeholder, e.g. $memberPlaceholder{}$, $memberFoo{}$ + /// + /// + /// + /// Expression placeholder arguments: + /// + /// expressionType - string value in single quotes, specifies full type name to match (empty string by default) + /// exactType - boolean value, specifies if expression should have exact type match (false by default) + /// + /// Examples: + /// + /// $myExpr{Expression, 'Namespace.FooType', true}$ - defines expression placeholder, matching expressions of the 'Namespace.FooType' type with exact matching. + /// $myExpr{Expression, 'Namespace.FooType'}$ - defines expression placeholder, matching expressions of the 'Namespace.FooType' type or expressions which can be implicitly converted to 'Namespace.FooType'. + /// $myExpr{Expression}$ - defines expression placeholder, matching expressions of any type. + /// $exprFoo{'Namespace.FooType', true}$ - defines expression placeholder, matching expressions of the 'Namespace.FooType' type with exact matching. + /// + /// + /// + /// Type placeholder arguments: + /// + /// type - string value in single quotes, specifies full type name to match (empty string by default) + /// exactType - boolean value, specifies if expression should have exact type match (false by default) + /// + /// Examples: + /// + /// $myType{Type, 'Namespace.FooType', true}$ - defines type placeholder, matching 'Namespace.FooType' types with exact matching. + /// $myType{Type, 'Namespace.FooType'}$ - defines type placeholder, matching 'Namespace.FooType' types or types, which can be implicitly converted to 'Namespace.FooType'. + /// $myType{Type}$ - defines type placeholder, matching any type. + /// $typeFoo{'Namespace.FooType', true}$ - defines types placeholder, matching 'Namespace.FooType' types with exact matching. + /// + /// + /// + /// Identifier placeholder arguments: + /// + /// nameRegex - string value in single quotes, specifies regex to use for matching (empty string by default) + /// nameRegexCaseSensitive - boolean value, specifies if name regex is case sensitive (true by default) + /// type - string value in single quotes, specifies full type name to match (empty string by default) + /// exactType - boolean value, specifies if expression should have exact type match (false by default) + /// + /// Examples: + /// + /// $myIdentifier{Identifier, 'my.*', false, 'Namespace.FooType', true}$ - defines identifier placeholder, matching identifiers (ignoring case) starting with 'my' prefix with 'Namespace.FooType' type. + /// $myIdentifier{Identifier, 'my.*', true, 'Namespace.FooType', true}$ - defines identifier placeholder, matching identifiers (case sensitively) starting with 'my' prefix with 'Namespace.FooType' type. + /// $identFoo{'my.*'}$ - defines identifier placeholder, matching identifiers (case sensitively) starting with 'my' prefix. + /// + /// + /// + /// Statement placeholder arguments: + /// + /// minimalOccurrences - minimal number of statements to match (-1 by default) + /// maximalOccurrences - maximal number of statements to match (-1 by default) + /// + /// Examples: + /// + /// $myStmt{Statement, 1, 2}$ - defines statement placeholder, matching 1 or 2 statements. + /// $myStmt{Statement}$ - defines statement placeholder, matching any number of statements. + /// $stmtFoo{1, 2}$ - defines statement placeholder, matching 1 or 2 statements. + /// + /// + /// + /// Argument placeholder arguments: + /// + /// minimalOccurrences - minimal number of arguments to match (-1 by default) + /// maximalOccurrences - maximal number of arguments to match (-1 by default) + /// + /// Examples: + /// + /// $myArg{Argument, 1, 2}$ - defines argument placeholder, matching 1 or 2 arguments. + /// $myArg{Argument}$ - defines argument placeholder, matching any number of arguments. + /// $argFoo{1, 2}$ - defines argument placeholder, matching 1 or 2 arguments. + /// + /// + /// + /// Member placeholder arguments: + /// + /// docId - string value in single quotes, specifies XML documentation id of the member to match (empty by default) + /// + /// Examples: + /// + /// $myMember{Member, 'M:System.String.IsNullOrEmpty(System.String)'}$ - defines member placeholder, matching 'IsNullOrEmpty' member of the 'System.String' type. + /// $memberFoo{'M:System.String.IsNullOrEmpty(System.String)'}$ - defines member placeholder, matching 'IsNullOrEmpty' member of the 'System.String' type. + /// + /// + /// + /// For more information please refer to the Structural Search and Replace article. + /// + /// + [AttributeUsage( + AttributeTargets.Method + | AttributeTargets.Constructor + | AttributeTargets.Property + | AttributeTargets.Field + | AttributeTargets.Event + | AttributeTargets.Interface + | AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Enum, + AllowMultiple = true, + Inherited = false)] + public sealed class CodeTemplateAttribute : Attribute + { + public CodeTemplateAttribute(string searchTemplate) + { + SearchTemplate = searchTemplate; + } + + /// + /// Structural search pattern to use in the code template. + /// Pattern includes textual part, which must contain only identifiers allowed in the target language, + /// and placeholders, which allow matching variable parts of the target code blocks. + /// + public string SearchTemplate { get; } + + /// + /// Message to show when the search pattern was found. + /// You can also prepend the message text with "Error:", "Warning:", "Suggestion:" or "Hint:" prefix to specify the pattern severity. + /// Code patterns with replace template produce suggestions by default. + /// However, if replace template is not provided, then warning severity will be used. + /// + public string Message { get; set; } + + /// + /// Structural search replace pattern to use in code template replacement. + /// + public string ReplaceTemplate { get; set; } + + /// + /// Replace message to show in the light bulb. + /// + public string ReplaceMessage { get; set; } + + /// + /// Apply code formatting after code replacement. + /// + public bool FormatAfterReplace { get; set; } = true; + + /// + /// Whether similar code blocks should be matched. + /// + public bool MatchSimilarConstructs { get; set; } + + /// + /// Automatically insert namespace import directives or remove qualifiers that become redundant after the template is applied. + /// + public bool ShortenReferences { get; set; } + + /// + /// String to use as a suppression key. + /// By default the following suppression key is used 'CodeTemplate_SomeType_SomeMember', + /// where 'SomeType' and 'SomeMember' are names of the associated containing type and member to which this attribute is applied. + /// + public string SuppressionKey { get; set; } + } +} diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/BaseEntity.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/BaseEntity.cs new file mode 100644 index 00000000..91929a85 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/BaseEntity.cs @@ -0,0 +1,30 @@ +using System.Reflection; + +namespace Lab.ChangeTracking.Domain.Annotations; + +public record BaseEntity : IChangeable +{ + private PropertyChangeTracker _tracker = new(); + + public void Initial() + { + this._tracker.Initial(); + } + + public bool HasChanged { get; private set; } + + public Dictionary GetChangedProperties() + { + return this._tracker.GetChangedProperties(); + } + + public Dictionary GetOriginalValues() + { + throw new NotImplementedException(); + } + + public void Track(string propertyName, object value) + { + this._tracker.Track(propertyName, value); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs new file mode 100644 index 00000000..ed22f194 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs @@ -0,0 +1,26 @@ +using ChangeTracking; +using Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; +using Lab.ChangeTracking.Domain.EmployeeAggregate.Repository; + +namespace Lab.ChangeTracking.Domain.EmployeeAggregate; + +public class EmployeeAggregate : IEmployeeAggregate +{ + private IEmployeeRepository _repository; + + public EmployeeAggregate(IEmployeeRepository repository) + { + this._repository = repository; + } + + public async Task ModifyFlowAsync(EmployeeEntity srcEmployee, CancellationToken cancel = default) + { + var memberTrackable = srcEmployee.AsTrackable(); + + memberTrackable.Name = "小章"; + memberTrackable.Identity.Password = "9527"; + + var changeCount = await this._repository.SaveChangeAsync(memberTrackable, cancel); + return memberTrackable; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs new file mode 100644 index 00000000..ac18ef3c --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs @@ -0,0 +1,26 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using ChangeTracking; +using Lab.ChangeTracking.Domain.Annotations; + +namespace Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; + +public record EmployeeEntity +{ + public virtual Guid Id { get; init; } + + public virtual string Name { get; set; } + + public virtual int? Age { get; set; } + + public virtual string Remark { get; set; } + + public virtual IList Profiles { get; init; } + + public virtual DateTimeOffset CreateAt { get; set; } + + public virtual string CreateBy { get; set; } + + public virtual IdentityEntity Identity { get; init; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity2.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity2.cs new file mode 100644 index 00000000..7171d53c --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity2.cs @@ -0,0 +1,49 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using ChangeTracking; +using Lab.ChangeTracking.Domain.Annotations; + +namespace Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; + +public record EmployeeEntity2 : BaseEntity +{ + private string _name; + private int? _age; + + public virtual Guid Id { get; init; } + + public virtual string Name + { + get => this._name; + init => this._name = value; + } + + public virtual int? Age + { + get => this._age; + init => this._age = value; + } + + public virtual string Remark { get; set; } + + public virtual DateTimeOffset CreateAt { get; set; } + + public virtual string CreateBy { get; set; } + + public EmployeeEntity2 SetName(string name) + { + this._name = name; + return this; + } + + public EmployeeEntity2 SetAge(int age) + { + if (this._age != age) + { + this._age = age; + this.Track(nameof(this.Age), age); + } + + return this; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs new file mode 100644 index 00000000..09505d0f --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs @@ -0,0 +1,14 @@ +namespace Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; + +public record IdentityEntity +{ + public virtual string Account { get; set; } + + public virtual string Password { get; set; } + + public virtual string Remark { get; set; } + + public virtual DateTimeOffset CreateAt { get; set; } + + public virtual string CreateBy { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/ProfileEntity.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/ProfileEntity.cs new file mode 100644 index 00000000..9ae35128 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/ProfileEntity.cs @@ -0,0 +1,8 @@ +namespace Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; + +public record ProfileEntity +{ + public virtual string FirstName { get; set; } + + public virtual string LastName { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs new file mode 100644 index 00000000..8af395df --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs @@ -0,0 +1,8 @@ +using Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; + +namespace Lab.ChangeTracking.Domain.EmployeeAggregate; + +public interface IEmployeeAggregate +{ + Task ModifyFlowAsync(EmployeeEntity employee, CancellationToken cancel = default); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs new file mode 100644 index 00000000..479d99cd --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs @@ -0,0 +1,120 @@ +using ChangeTracking; +using EFCore.BulkExtensions; +using Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; + +namespace Lab.ChangeTracking.Domain.EmployeeAggregate.Repository; + +public class EmployeeRepository : IEmployeeRepository +{ + private readonly IDbContextFactory _memberContextFactory; + + public EmployeeRepository(IDbContextFactory memberContextFactory) + { + this._memberContextFactory = memberContextFactory; + } + + public Employee To(EmployeeEntity srcEmployee) + { + return new Employee + { + Id = srcEmployee.Id, + Name = srcEmployee.Name, + Age = srcEmployee.Age, + Remark = srcEmployee.Remark, + CreateAt = srcEmployee.CreateAt, + CreateBy = srcEmployee.CreateBy, + Identity = this.To(srcEmployee.Identity) + }; + } + + public Identity To(IdentityEntity srcIdentity) + { + return new Identity + { + Password = srcIdentity.Password, + Remark = srcIdentity.Remark, + CreateAt = srcIdentity.CreateAt, + CreateBy = srcIdentity.CreateBy, + }; + } + + public async Task SaveChangeAsync(EmployeeEntity srcEmployee, + CancellationToken cancel = default) + { + var employeeTrackable = srcEmployee.CastToIChangeTrackable(); + var identityTrackable = srcEmployee.Identity.CastToIChangeTrackable(); + var memberChangeProperties = employeeTrackable.ChangedProperties.ToList(); + var identityChangeProperties = identityTrackable.ChangedProperties.ToList(); + + await using var dbContext = await this._memberContextFactory.CreateDbContextAsync(cancel); + await using var transaction = await dbContext.Database.BeginTransactionAsync(cancel); + + try + { + var destEmployee = this.To(srcEmployee); + var memberChangeCount = await dbContext.Employees + .Where(a => a.Id == srcEmployee.Id) + .BatchUpdateAsync(destEmployee, + memberChangeProperties, cancel); + var identityChangeCount = await dbContext.Identities + .Where(a => a.Employee_Id == srcEmployee.Id) + .BatchUpdateAsync(destEmployee.Identity, + identityChangeProperties, + cancel); + + await transaction.CommitAsync(cancel); + return memberChangeCount + identityChangeCount; + } + catch (Exception e) + { + await transaction.RollbackAsync(cancel); + throw new Exception("存檔失敗"); + } + + return 0; + } + + public async Task SaveChange1Async(EmployeeEntity srcEmployee, + CancellationToken cancel = default) + { + var employeeTrackable = srcEmployee.CastToIChangeTrackable(); + var identityTrackable = srcEmployee.Identity.CastToIChangeTrackable(); + var profileTrackable = srcEmployee.Profiles.CastToIChangeTrackableCollection(); + + var memberChangeProperties = employeeTrackable.ChangedProperties.ToList(); + var identityChangeProperties = identityTrackable.ChangedProperties.ToList(); + var changedItems = profileTrackable.ChangedItems; + var addedItems = profileTrackable.AddedItems; + var unchangedItems = profileTrackable.UnchangedItems; + var deletedItems = profileTrackable.DeletedItems; + + await using var dbContext = await this._memberContextFactory.CreateDbContextAsync(cancel); + await using var transaction = await dbContext.Database.BeginTransactionAsync(cancel); + + try + { + var destEmployee = this.To(srcEmployee); + var memberChangeCount = await dbContext.Employees + .Where(a => a.Id == srcEmployee.Id) + .BatchUpdateAsync(destEmployee, + memberChangeProperties, cancel); + var identityChangeCount = await dbContext.Identities + .Where(a => a.Employee_Id == srcEmployee.Id) + .BatchUpdateAsync(destEmployee.Identity, + identityChangeProperties, + cancel); + + await transaction.CommitAsync(cancel); + return memberChangeCount + identityChangeCount; + } + catch (Exception e) + { + await transaction.RollbackAsync(cancel); + throw new Exception("存檔失敗"); + } + + return 0; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs new file mode 100644 index 00000000..3fd851d5 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs @@ -0,0 +1,8 @@ +using Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; + +namespace Lab.ChangeTracking.Domain.EmployeeAggregate.Repository; + +public interface IEmployeeRepository +{ + Task SaveChangeAsync(EmployeeEntity employee, CancellationToken cancel = default); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/IChangeable.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/IChangeable.cs new file mode 100644 index 00000000..65415d74 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/IChangeable.cs @@ -0,0 +1,14 @@ +namespace Lab.ChangeTracking.Domain.Annotations; + +public interface IChangeable +{ + Dictionary GetChangedProperties(); + + Dictionary GetOriginalValues(); + + void Track(string propertyName, object value); + + void Initial(); + + bool HasChanged { get; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj new file mode 100644 index 00000000..31373e41 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/PropertyChangeTracker.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/PropertyChangeTracker.cs new file mode 100644 index 00000000..12577d8b --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/PropertyChangeTracker.cs @@ -0,0 +1,41 @@ +using System.Reflection; + +namespace Lab.ChangeTracking.Domain.Annotations; + +public class PropertyChangeTracker +{ + private Dictionary _changedProperties = new(); + private Dictionary _originalValues = new(); + + public void Initial() + { + var properties = this.GetType().GetProperties(); + foreach (var property in properties) + { + this._originalValues.Add(property.Name, property.GetValue(this)); + } + } + + public Dictionary GetChangedProperties() + { + return this._changedProperties; + } + + public Dictionary GetOriginalValues() + { + throw new NotImplementedException(); + } + + public void Track(string propertyName, object value) + { + var changes = this._changedProperties; + if (changes.ContainsKey(propertyName) == false) + { + changes.Add(propertyName, value); + } + else + { + changes[propertyName] = value; + } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/Survey.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/Survey.cs new file mode 100644 index 00000000..dfa0a859 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Domain/Survey.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; +using System.Reflection; + +namespace Lab.ChangeTracking.Domain.Annotations; + +public class Base : IRevertibleChangeTracking +{ + protected readonly Dictionary ChangedProperties = new(); + protected readonly Dictionary OriginalValues = new(); + + public void Initialize() + { + var properties = this.GetType().GetProperties(); + + // Save the current value of the properties to our dictionary. + foreach (var property in properties) + { + this.OriginalValues.Add(property.Name, property.GetValue(this)); + } + } + + public bool IsChanged { get; private set; } + + public void RejectChanges() + { + foreach (var property in this.ChangedProperties) + { + this.GetType().GetRuntimeProperty(property.Key).SetValue(this, property.Value); + } + + this.AcceptChanges(); + } + + public void AcceptChanges() + { + this.ChangedProperties.Clear(); + this.IsChanged = false; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs new file mode 100644 index 00000000..6fc51c5c --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs @@ -0,0 +1,50 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.ChangeTracking.Infrastructure.DB; + +public static class AppDependencyInjectionExtensions +{ + public static void AddAppEnvironment(this IServiceCollection services) + { + services.AddLogging(builder => + { + builder.AddConsole(); + }); + services.AddSingleton(); + } + + public static void AddEntityFramework(this IServiceCollection services) + { + services.AddPooledDbContextFactory((provider, optionsBuilder) => + { + var option = provider.GetService(); + var connectionString = option.EmployeeDbConnectionString; + var loggerFactory = provider.GetService(); + optionsBuilder.UseSqlServer(connectionString) + .UseLoggerFactory(loggerFactory) + ; + }); + + // services.AddPooledDbContextFactory((provider, options) => + // { + // var option = provider.GetService(); + // var loggerFactory = provider.GetService(); + // options.UseNpgsql( + // option.MemberDbConnectionString, //只會呼叫一次 + // builder => + // builder.EnableRetryOnFailure( + // 10, + // TimeSpan.FromSeconds(30), + // new List { "57P01" })) + // + // // .UseLazyLoadingProxies() + // .UseSnakeCaseNamingConvention() + // .UseLoggerFactory(loggerFactory) + // ; + // }); + } + +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs new file mode 100644 index 00000000..e29f53f3 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs @@ -0,0 +1,31 @@ +namespace Lab.ChangeTracking.Infrastructure.DB; + +public class AppEnvironmentOption +{ + public string EmployeeDbConnectionString + { + get + { + if (string.IsNullOrWhiteSpace(this._employeeDbConnectionString)) + { + this._employeeDbConnectionString = + EnvironmentAssistant.GetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR); + } + + return this._employeeDbConnectionString; + } + set + { + this._employeeDbConnectionString = value; + Environment.SetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR, value); + } + } + + private string _employeeDbConnectionString; + private readonly string EMPLOYEE_DB_CONN_STR = "EMPLOYEE_DB_CONNECTION_STR"; + + public void Initial() + { + var memberDbConnectionString = this.EmployeeDbConnectionString; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs new file mode 100644 index 00000000..6bc11f3c --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + [Table("Employee")] + public class Employee + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Id { get; set; } + + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + public string Remark { get; set; } + + [Required] + public DateTimeOffset CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + public virtual Identity Identity { get; set; } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..15a2302d --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.InMemory.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = { false }; + + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identities { get; set; } + + public virtual DbSet OrderHistories { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + if (s_migrated[0]) + { + return; + } + + lock (s_migrated) + { + if (s_migrated[0] == false) + { + var memoryOptions = options.FindExtension(); + + if (memoryOptions == null) + { + var sqlOptions = options.FindExtension(); + if (sqlOptions != null) + { + this.Database.Migrate(); + } + } + + s_migrated[0] = true; + } + } + } + + //管理索引 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(p => + { + p.HasKey(e => e.Id) + .IsClustered(false); + + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + + p.Property(p => p.Remark) + .IsRequired(false) + ; + }); + + modelBuilder.Entity(p => + { + p.HasKey(e => e.Employee_Id) + .IsClustered(false); + + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + + p.Property(p => p.Remark) + .IsRequired(false) + ; + }); + } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs new file mode 100644 index 00000000..fc557c2b --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + [Table("Identity")] + public class Identity + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Employee_Id { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Remark { get; set; } + + [Required] + public DateTimeOffset CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + [ForeignKey("Employee_Id")] + public virtual Employee Employee { get; set; } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/OrderHistory.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/OrderHistory.cs new file mode 100644 index 00000000..914d8af6 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/OrderHistory.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + [Table("OrderHistory")] + public class OrderHistory + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public Guid? Employee_Id { get; set; } + + public string Remark { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Product_Id { get; set; } + + public string Product_Name { get; set; } + + [Required] + public DateTime CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs new file mode 100644 index 00000000..b115d8a2 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs @@ -0,0 +1,15 @@ +namespace Lab.ChangeTracking.Infrastructure.DB; + +public class EnvironmentAssistant +{ + public static string GetEnvironmentVariable(string key) + { + var result = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrWhiteSpace(result)) + { + throw new Exception($"the key '{key}' not exist in environment variable"); + } + + return result; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj new file mode 100644 index 00000000..9fcdb0d8 --- /dev/null +++ b/Property Change Tracking/ChangeTracking/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + + + + + + + + + diff --git a/Property Change Tracking/ChangeTracking2/ChangeTracking2.sln b/Property Change Tracking/ChangeTracking2/ChangeTracking2.sln new file mode 100644 index 00000000..030aecc7 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/ChangeTracking2.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{1F776B7D-C182-4DC3-BA57-844657BD9AC0}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + Makefile = Makefile + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{483A9EB7-C4AF-4D68-80ED-49277985C760}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Infrastructure.DB", "src\Lab.ChangeTracking.Infrastructure.DB\Lab.ChangeTracking.Infrastructure.DB.csproj", "{A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Domain", "src\Lab.ChangeTracking.Domain\Lab.ChangeTracking.Domain.csproj", "{A1C827F2-683E-470C-A3DE-BD6DD2BE5198}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Domain.UnitTest", "src\Lab.ChangeTracking.Domain.UnitTest\Lab.ChangeTracking.Domain.UnitTest.csproj", "{7066EE2C-28A8-4408-B212-E582608AB967}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Abstract", "src\Lab.ChangeTracking.Abstract\Lab.ChangeTracking.Abstract.csproj", "{43C40083-77B5-4068-A707-1993D3B29410}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Release|Any CPU.Build.0 = Release|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Release|Any CPU.Build.0 = Release|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Release|Any CPU.Build.0 = Release|Any CPU + {43C40083-77B5-4068-A707-1993D3B29410}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43C40083-77B5-4068-A707-1993D3B29410}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43C40083-77B5-4068-A707-1993D3B29410}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43C40083-77B5-4068-A707-1993D3B29410}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {7066EE2C-28A8-4408-B212-E582608AB967} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {43C40083-77B5-4068-A707-1993D3B29410} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + EndGlobalSection +EndGlobal diff --git a/Property Change Tracking/ChangeTracking2/Makefile b/Property Change Tracking/ChangeTracking2/Makefile new file mode 100644 index 00000000..cf17f07f --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/Makefile @@ -0,0 +1,4 @@ +G1: + echo 'Hello World' +G2: + cmd \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/docker-compose.yml b/Property Change Tracking/ChangeTracking2/docker-compose.yml new file mode 100644 index 00000000..8ecb1567 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.8" + +services: + db-sql: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=pass@w0rd1~ + ports: + - 1433:1433 \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Abstract/IEntity.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Abstract/IEntity.cs new file mode 100644 index 00000000..1076bfc0 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Abstract/IEntity.cs @@ -0,0 +1,6 @@ +// namespace Lab.ChangeTracking.Abstract; +// +// public interface IEntity +// { +// Guid Id { get; set; } +// } \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Abstract/Lab.ChangeTracking.Abstract.csproj b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Abstract/Lab.ChangeTracking.Abstract.csproj new file mode 100644 index 00000000..eb2460e9 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Abstract/Lab.ChangeTracking.Abstract.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs new file mode 100644 index 00000000..9942df35 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Unicode; +using ChangeTracking; +using EFCore.BulkExtensions; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.ChangeTracking.Domain.UnitTest; + +[TestClass] +public class ChangeTrackingUnitTest +{ + private readonly IEmployeeAggregate _employeeAggregate = TestAssistants.EmployeeAggregate; + + private static readonly IDbContextFactory s_employeeDbContextFactory = + TestAssistants.EmployeeDbContextFactory; + + private readonly IEmployeeRepository _employeeRepository = TestAssistants.EmployeeRepository; + + [TestMethod] + public void 異動追蹤後存檔() + { + var employeeEntity = Insert().To(); + var trackable = employeeEntity.AsTrackable(); + trackable.Age = 20; + trackable.Name = "小章"; + trackable.Remark = "我變了"; + trackable.Identity.Remark = "我變了"; + trackable.Addresses[0].Remark = "我變了"; + trackable.Addresses.RemoveAt(1); + trackable.Addresses.Add(new AddressEntity() + { + Id = Guid.NewGuid(), + Employee_Id = employeeEntity.Id, + CreatedAt = DateTimeOffset.Now, + CreatedBy = "sys", + Country = "Taipei", + Street = "Street", + Remark = "我新的" + }); + var count = this._employeeRepository.SaveChangeAsync(trackable).Result; + var dbContext = s_employeeDbContextFactory.CreateDbContext(); + var actual = dbContext.Employees + .Where(p => p.Id == employeeEntity.Id) + .Include(p => p.Identity) + .Include(p => p.Addresses) + .First() + ; + Assert.AreEqual("我變了",actual.Remark); + Assert.AreEqual("我變了",actual.Identity.Remark); + Assert.AreEqual("我變了",actual.Addresses[0].Remark); + Assert.AreEqual("我新的",actual.Addresses[1].Remark); + } + + [TestMethod] + public void 異動追蹤後存檔_回傳不可變的物件() + { + // var toDB = Insert(); + // var source = new EmployeeEntity + // { + // Id = toDB.Id, + // Name = "yao", + // Age = 12, + // Identity = new IdentityEntity + // { + // Employee_Id = toDB.Identity.Employee_Id + // }, + // Addresses = new List + // { + // new() + // { + // Id = toDB.Addresses[0] + // .Id, + // Employee_Id = toDB.Id, + // Remark = "AAA" + // }, + // new() + // { + // Id = toDB.Addresses[1] + // .Id, + // Employee_Id = toDB.Id, + // Remark = "AAA" + // } + // } + // }; + // var employeeEntity = this._employeeAggregate.ModifyFlowAsync(source).Result; + // this.DataShouldOk(source); + } + + private void DataShouldOk(EmployeeEntity source) + { + var dbContext = s_employeeDbContextFactory.CreateDbContext(); + var actual = dbContext.Employees + .Where(p => p.Id == source.Id) + .Include(p => p.Identity) + .First() + ; + + Assert.AreEqual("小章", actual.Name); + Assert.AreEqual("9527", actual.Identity.Password); + } + + private static Employee Insert() + { + using var dbContext = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + var employeeId = Guid.NewGuid(); + var toDB = new Employee + { + Id = employeeId, + Age = 18, + Name = "yao", + CreatedAt = DateTimeOffset.Now, + CreatedBy = "Sys", + Identity = new Identity + { + Employee_Id = employeeId, + Account = "yao", + Password = "123456", + CreatedAt = DateTimeOffset.Now, + CreatedBy = "Sys", + Remark = "編輯" + }, + Addresses = new List
+ { + new() + { + Id = Guid.NewGuid(), + Employee_Id = employeeId, + CreatedAt = DateTimeOffset.Now, + CreatedBy = "sys", + Country = "Taipei", + Street = "Street", + Remark = "修改的" + }, + new() + { + Id = Guid.NewGuid(), + Employee_Id = employeeId, + CreatedAt = DateTimeOffset.Now, + CreatedBy = "sys", + Country = "Taipei", + Street = "Street", + Remark = "刪除的" + } + } + }; + dbContext.Employees.Add(toDB); + dbContext.SaveChanges(); + return toDB; + } + [ClassCleanup] + public static void ClassCleanup() + { + DeleteAllTable(); + } + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + DeleteAllTable(); + } + private static void DeleteAllTable() + { + var dbContext = s_employeeDbContextFactory.CreateDbContext(); + dbContext.Employees.BatchDelete(); + dbContext.Addresses.BatchDelete(); + dbContext.Identity.BatchDelete(); + } + private static string ToJson(T instance) + { + var serialize = JsonSerializer.Serialize(instance, + new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.Create( + UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs) + }); + return serialize; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj new file mode 100644 index 00000000..fbfda1f2 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs new file mode 100644 index 00000000..817754fe --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs @@ -0,0 +1,31 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.ChangeTracking.Domain.UnitTest; + +[TestClass] +public class MsTestHook +{ + [AssemblyCleanup] + public static void Cleanup() + { + TestAssistants.SetTestEnvironmentVariable(); + var db = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + } + + [AssemblyInitialize] + public static void Setup(TestContext context) + { + TestAssistants.SetTestEnvironmentVariable(); + var db = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + + db.Database.EnsureCreated(); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs new file mode 100644 index 00000000..d9741d2a --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs @@ -0,0 +1,46 @@ +using System; +using Lab.ChangeTracking.Infrastructure.DB; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Lab.ChangeTracking.Domain.UnitTest; + +// assistant +internal class TestAssistants +{ + private static IServiceProvider _serviceProvider; + + public static IDbContextFactory EmployeeDbContextFactory => + _serviceProvider.GetService>(); + + public static IEmployeeRepository EmployeeRepository => + _serviceProvider.GetService(); + + public static IEmployeeAggregate EmployeeAggregate => + _serviceProvider.GetService(); + + static TestAssistants() + { + var services = new ServiceCollection(); + ConfigureTestServices(services); + SetTestEnvironmentVariable(); + } + + public static void ConfigureTestServices(IServiceCollection services) + { + services.AddAppEnvironment(); + services.AddEntityFramework(); + + services.AddSingleton(); + services.AddSingleton(); + _serviceProvider = services.BuildServiceProvider(); + } + + public static void SetTestEnvironmentVariable() + { + var option = _serviceProvider.GetService(); + option.EmployeeDbConnectionString = + "Data Source=localhost;Initial Catalog=EmployeeDb;Integrated Security=false;User ID=sa;Password=pass@w0rd1~;MultipleActiveResultSets=True;TrustServerCertificate=True"; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Aggregate/EmployeeAggregate.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Aggregate/EmployeeAggregate.cs new file mode 100644 index 00000000..875cfc6c --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Aggregate/EmployeeAggregate.cs @@ -0,0 +1,25 @@ +using ChangeTracking; + +namespace Lab.ChangeTracking.Domain; + +public class EmployeeAggregate : IEmployeeAggregate +{ + private readonly IEmployeeRepository _repository; + + public EmployeeAggregate(IEmployeeRepository repository) + { + this._repository = repository; + } + + public async Task ModifyFlowAsync(EmployeeEntity srcEmployee, CancellationToken cancel = default) + { + var memberTrackable = srcEmployee.AsTrackable(); + + memberTrackable.Remark = "我變了"; + memberTrackable.Identity.Remark = "我變了"; + memberTrackable.Addresses[0].Remark = "我變了"; + memberTrackable.Addresses.RemoveAt(1); + var changeCount = await this._repository.SaveChangeAsync(memberTrackable, cancel); + return memberTrackable; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Aggregate/IEmployeeAggregate.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Aggregate/IEmployeeAggregate.cs new file mode 100644 index 00000000..224c9362 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Aggregate/IEmployeeAggregate.cs @@ -0,0 +1,6 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IEmployeeAggregate +{ + Task ModifyFlowAsync(EmployeeEntity employee, CancellationToken cancel = default); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Entity/AddressEntity.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Entity/AddressEntity.cs new file mode 100644 index 00000000..5e8d45e1 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Entity/AddressEntity.cs @@ -0,0 +1,23 @@ + +namespace Lab.ChangeTracking.Domain; + +public record AddressEntity +{ + public virtual Guid Id { get; set; } + + public virtual Guid Employee_Id { get; set; } + + public virtual string Country { get; set; } + + public virtual string Street { get; set; } + + public virtual DateTimeOffset CreatedAt { get; set; } + + public virtual string CreatedBy { get; set; } + + public virtual DateTimeOffset? ModifiedAt { get; set; } + + public virtual string ModifiedBy { get; set; } + + public virtual string Remark { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Entity/EmployeeEntity.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Entity/EmployeeEntity.cs new file mode 100644 index 00000000..5bd37ec2 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Entity/EmployeeEntity.cs @@ -0,0 +1,26 @@ +namespace Lab.ChangeTracking.Domain; + +public record EmployeeEntity +{ + public virtual Guid Id { get; set; } + + public virtual string Name { get; set; } + + public virtual int? Age { get; set; } + + public virtual int Version { get; set; } + + public virtual IdentityEntity Identity { get; set; } + + public virtual IList Addresses { get; set; } + + public virtual DateTimeOffset CreatedAt { get; set; } + + public virtual string CreatedBy { get; set; } + + public virtual DateTimeOffset? ModifiedAt { get; set; } + + public virtual string ModifiedBy { get; set; } + + public virtual string Remark { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Entity/IdentityEntity.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Entity/IdentityEntity.cs new file mode 100644 index 00000000..8982147c --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Entity/IdentityEntity.cs @@ -0,0 +1,22 @@ + +namespace Lab.ChangeTracking.Domain; + +public record IdentityEntity +{ + public virtual Guid Employee_Id { get; set; } + + public virtual string Account { get; set; } + + public virtual string Password { get; set; } + + public virtual DateTimeOffset CreatedAt { get; set; } + + public virtual string CreatedBy { get; set; } + + public virtual DateTimeOffset? ModifiedAt { get; set; } + + public virtual string? ModifiedBy { get; set; } + + public virtual string Remark { get; set; } + +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/EmployeeRepository.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/EmployeeRepository.cs new file mode 100644 index 00000000..cde717bc --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/EmployeeRepository.cs @@ -0,0 +1,29 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; + +namespace Lab.ChangeTracking.Domain; + +public class EmployeeRepository : RepositoryBase, IEmployeeRepository +{ + private readonly IDbContextFactory _memberContextFactory; + + public EmployeeRepository(IDbContextFactory memberContextFactory) + { + this._memberContextFactory = memberContextFactory; + } + + public async Task SaveChangeAsync(EmployeeEntity srcEmployee, + CancellationToken cancel = default) + { + await using var dbContext = await this._memberContextFactory.CreateDbContextAsync(cancel); + this.ApplyModify(dbContext, srcEmployee, new List + { + "Identity", + "Addresses" + } + ); + this.ApplyModify(dbContext, srcEmployee.Identity); + this.ApplyChanges(dbContext, srcEmployee.Addresses); + return await dbContext.SaveChangesAsync(cancel); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/IEmployeeRepository.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/IEmployeeRepository.cs new file mode 100644 index 00000000..8748817c --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/IEmployeeRepository.cs @@ -0,0 +1,6 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IEmployeeRepository +{ + Task SaveChangeAsync(EmployeeEntity employee, CancellationToken cancel = default); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/RepositoryBase.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/RepositoryBase.cs new file mode 100644 index 00000000..16d5ff97 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/RepositoryBase.cs @@ -0,0 +1,128 @@ +using ChangeTracking; +using Microsoft.EntityFrameworkCore; + +namespace Lab.ChangeTracking.Domain; + +public class RepositoryBase +{ + protected void ApplyAdd(DbContext dbContext, + TSource sourceInstance, + IEnumerable excludeProperties = null) + where TSource : class + where TTarget : class + { + var targetInstance = CreateNewInstance(sourceInstance, excludeProperties); + dbContext.Entry(targetInstance).State = EntityState.Added; + } + + protected void ApplyAdd(DbContext dbContext, TSource source) + where TSource : class where TTarget : class + { + var targetInstance = CreateDeleteInstance(source, "Id"); + dbContext.Set().Attach(targetInstance); + dbContext.Entry(targetInstance).State = EntityState.Deleted; + } + + protected void ApplyChanges(DbContext dbContext, + IList sources, + IEnumerable excludeProperties = null) + where TSource : class + where TTarget : class + + { + var targetsTrackable = sources.CastToIChangeTrackableCollection(); + if (targetsTrackable == null) + { + return; + } + + var modifyItems = targetsTrackable.ChangedItems; + var addedItems = targetsTrackable.AddedItems; + var deletedItems = targetsTrackable.DeletedItems; + foreach (var source in modifyItems) + { + this.ApplyModify(dbContext, source, excludeProperties); + } + + foreach (var addedItem in addedItems) + { + this.ApplyAdd(dbContext, addedItem, excludeProperties); + } + + foreach (var source in deletedItems) + { + this.ApplyAdd(dbContext, source); + } + } + + protected void ApplyModify(DbContext dbContext, + TSource source, + IEnumerable excludeProperties = null) + where TSource : class + where TTarget : class + { + var sourceTrackable = source.CastToIChangeTrackable(); + var targetInstance = CreateNewInstance(source, excludeProperties); + dbContext.Set().Attach(targetInstance); + + var changedProperties = sourceTrackable.ChangedProperties; + foreach (var changedProperty in changedProperties) + { + if (excludeProperties != null + && excludeProperties.Any(p => p == changedProperty)) + { + continue; + } + + dbContext.Entry(targetInstance).Property(changedProperty).IsModified = true; + } + } + + private static TTarget CreateDeleteInstance(TSource sourceInstance, string propertyName) + where TSource : class + where TTarget : class + { + var targetType = typeof(TTarget); + var targetInstance = (TTarget)Activator.CreateInstance(targetType); + var targetProperty = targetType.GetProperty(propertyName); + var sourceType = sourceInstance.GetType(); + var sourceProperty = sourceType.GetProperty(propertyName); + var value = sourceProperty.GetValue(sourceInstance, null); + targetProperty.SetValue(targetInstance, value, null); + + return targetInstance; + } + + private static TTarget CreateNewInstance(TSource sourceInstance, + IEnumerable excludeProperties = null) + where TSource : class + where TTarget : class + { + var targetType = typeof(TTarget); + var targetInstance = (TTarget)Activator.CreateInstance(targetType); + var targetProperties = targetInstance.GetType().GetProperties(); + var sourceType = typeof(TSource); + var sourceProperties = sourceType.GetProperties(); + foreach (var sourceProperty in sourceProperties) + { + if (excludeProperties != null && + excludeProperties.Contains(sourceProperty.Name)) + { + continue; + } + + foreach (var targetProperty in targetProperties) + { + if (sourceProperty.Name == targetProperty.Name + & sourceProperty.PropertyType == targetProperty.PropertyType) + { + var value = sourceProperty.GetValue(sourceInstance, null); + targetProperty.SetValue(targetInstance, value, null); + break; + } + } + } + + return targetInstance; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/TypeConverterExtensions.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/TypeConverterExtensions.cs new file mode 100644 index 00000000..8e8ae5d7 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Employee/Repository/TypeConverterExtensions.cs @@ -0,0 +1,138 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; + +namespace Lab.ChangeTracking.Domain; + +public static class TypeConverterExtensions +{ + public static Employee To(this EmployeeEntity srcEmployee) + { + if (srcEmployee == null) + { + return null; + } + + return new Employee + { + Id = srcEmployee.Id, + Name = srcEmployee.Name, + Age = srcEmployee.Age, + Version = srcEmployee.Version, + Remark = srcEmployee.Remark, + Addresses = srcEmployee.Addresses.To()?.ToList(), + Identity = srcEmployee.Identity.To(), + CreatedAt = srcEmployee.CreatedAt, + CreatedBy = srcEmployee.CreatedBy, + ModifiedAt = srcEmployee.ModifiedAt, + ModifiedBy = srcEmployee.ModifiedBy + }; + } + public static EmployeeEntity To(this Employee srcEmployee) + { + if (srcEmployee == null) + { + return null; + } + + return new EmployeeEntity + { + Id = srcEmployee.Id, + Name = srcEmployee.Name, + Age = srcEmployee.Age, + Version = srcEmployee.Version, + Remark = srcEmployee.Remark, + Addresses = srcEmployee.Addresses.To()?.ToList(), + Identity = srcEmployee.Identity.To(), + CreatedAt = srcEmployee.CreatedAt, + CreatedBy = srcEmployee.CreatedBy, + ModifiedAt = srcEmployee.ModifiedAt, + ModifiedBy = srcEmployee.ModifiedBy + }; + } + public static Identity To(this IdentityEntity srcIdentity) + { + if (srcIdentity == null) + { + return null; + } + + return new Identity + { + Employee_Id = srcIdentity.Employee_Id, + Account = srcIdentity.Account, + Password = srcIdentity.Password, + Remark = srcIdentity.Remark, + CreatedAt = srcIdentity.CreatedAt, + CreatedBy = srcIdentity.CreatedBy, + ModifiedAt = srcIdentity.ModifiedAt, + ModifiedBy = srcIdentity.ModifiedBy + }; + } + public static IdentityEntity To(this Identity srcIdentity) + { + if (srcIdentity == null) + { + return null; + } + + return new IdentityEntity + { + Employee_Id = srcIdentity.Employee_Id, + Account = srcIdentity.Account, + Password = srcIdentity.Password, + Remark = srcIdentity.Remark, + CreatedAt = srcIdentity.CreatedAt, + CreatedBy = srcIdentity.CreatedBy, + ModifiedAt = srcIdentity.ModifiedAt, + ModifiedBy = srcIdentity.ModifiedBy + }; + } + public static Address To(this AddressEntity srcAddress) + { + if (srcAddress == null) + { + return null; + } + + return new Address + { + Id = srcAddress.Id, + Employee_Id = srcAddress.Employee_Id, + Country = srcAddress.Country, + Street = srcAddress.Street, + CreatedAt = srcAddress.CreatedAt, + CreatedBy = srcAddress.CreatedBy, + ModifiedAt = srcAddress.ModifiedAt, + ModifiedBy = srcAddress.ModifiedBy, + Remark = srcAddress.Remark + }; + } + public static AddressEntity To(this Address srcAddress) + { + if (srcAddress == null) + { + return null; + } + + return new AddressEntity + { + Id = srcAddress.Id, + Employee_Id = srcAddress.Employee_Id, + Country = srcAddress.Country, + Street = srcAddress.Street, + CreatedAt = srcAddress.CreatedAt, + CreatedBy = srcAddress.CreatedBy, + ModifiedAt = srcAddress.ModifiedAt, + ModifiedBy = srcAddress.ModifiedBy, + Remark = srcAddress.Remark + }; + } + public static IEnumerable
To(this IEnumerable srcProfiles) + { + return srcProfiles?.Select(p => p?.To()); + } + + public static IEnumerable To(this IEnumerable
srcProfiles) + { + return srcProfiles?.Select(p => p?.To()); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj new file mode 100644 index 00000000..def7c560 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs new file mode 100644 index 00000000..9d906132 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs @@ -0,0 +1,48 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.ChangeTracking.Infrastructure.DB; + +public static class AppDependencyInjectionExtensions +{ + public static void AddAppEnvironment(this IServiceCollection services) + { + services.AddLogging(builder => { builder.AddConsole(); }); + services.AddSingleton(); + } + + public static void AddEntityFramework(this IServiceCollection services) + { + services.AddPooledDbContextFactory((provider, optionsBuilder) => + { + var option = provider.GetService(); + var connectionString = option.EmployeeDbConnectionString; + var loggerFactory = provider.GetService(); + optionsBuilder.UseSqlServer(connectionString) + .LogTo(Console.WriteLine) + .EnableSensitiveDataLogging() + .UseLoggerFactory(loggerFactory) + ; + }); + + // services.AddPooledDbContextFactory((provider, options) => + // { + // var option = provider.GetService(); + // var loggerFactory = provider.GetService(); + // options.UseNpgsql( + // option.MemberDbConnectionString, //只會呼叫一次 + // builder => + // builder.EnableRetryOnFailure( + // 10, + // TimeSpan.FromSeconds(30), + // new List { "57P01" })) + // + // // .UseLazyLoadingProxies() + // .UseSnakeCaseNamingConvention() + // .UseLoggerFactory(loggerFactory) + // ; + // }); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs new file mode 100644 index 00000000..d93a85cc --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs @@ -0,0 +1,32 @@ +namespace Lab.ChangeTracking.Infrastructure.DB; + +public class AppEnvironmentOption +{ + public string EmployeeDbConnectionString + { + get + { + if (string.IsNullOrWhiteSpace(this._employeeDbConnectionString)) + { + this._employeeDbConnectionString = + EnvironmentAssistant.GetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR); + } + + return this._employeeDbConnectionString; + } + set + { + this._employeeDbConnectionString = value; + Environment.SetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR, value); + } + } + + private readonly string EMPLOYEE_DB_CONN_STR = "EMPLOYEE_DB_CONNECTION_STR"; + + private string _employeeDbConnectionString; + + public void Initial() + { + var memberDbConnectionString = this.EmployeeDbConnectionString; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Address.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Address.cs new file mode 100644 index 00000000..cdddbfa0 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Address.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel; + +public class Address +{ + public Guid Id { get; set; } + + public Guid Employee_Id { get; set; } + + public Employee Employee { get; set; } + + public string Country { get; set; } + + public string Street { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string ModifiedBy { get; set; } + + public string Remark { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int SequenceId { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs new file mode 100644 index 00000000..42bc21b0 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class Employee + { + public Guid Id { get; set; } + + public int Version { get; set; } + + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + public IList
Addresses { get; set; } + + public Identity Identity { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string ModifiedBy { get; set; } + + public string Remark { get; set; } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..9ecfbc28 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,120 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = { false }; + + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identity { get; set; } + + public virtual DbSet
Addresses { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + // 改用 CI 執行 Migrate + + // if (s_migrated[0]) + // { + // return; + // } + // + // lock (s_migrated) + // { + // if (s_migrated[0] == false) + // { + // var sqlOptions = options.FindExtension(); + // if (sqlOptions != null) + // { + // this.Database.Migrate(); + // } + // + // s_migrated[0] = true; + // } + // } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new EmployeeConfiguration()); + modelBuilder.ApplyConfiguration(new IdentityConfiguration()); + modelBuilder.ApplyConfiguration(new AddressConfiguration()); + } + + internal class EmployeeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Employee"); + builder.HasKey(p => p.Id) + .IsClustered(false); + + builder.Property(p => p.Name).IsRequired(); + builder.Property(p => p.CreatedBy).IsRequired(); + builder.Property(p => p.CreatedAt).IsRequired(); + builder.Property(p => p.ModifiedAt).IsRequired(false); + builder.Property(p => p.ModifiedBy).IsRequired(false); + builder.Property(p => p.Remark).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + + private class IdentityConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Identity"); + builder.HasKey(p => p.Employee_Id).IsClustered(false); + builder.HasOne(e => e.Employee) + .WithOne(p => p.Identity) + .HasForeignKey(p => p.Employee_Id) + .OnDelete(DeleteBehavior.Cascade) + ; + + builder.Property(p => p.Account).IsRequired(); + builder.Property(p => p.Password).IsRequired(); + builder.Property(p => p.CreatedBy).IsRequired(); + builder.Property(p => p.CreatedAt).IsRequired(); + builder.Property(p => p.ModifiedAt).IsRequired(false); + builder.Property(p => p.ModifiedBy).IsRequired(false); + builder.Property(p => p.Remark).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + + private class AddressConfiguration : IEntityTypeConfiguration
+ { + public void Configure(EntityTypeBuilder
builder) + { + builder.ToTable("Address"); + builder.HasKey(p => p.Id).IsClustered(false); + builder.HasOne(e => e.Employee) + .WithMany(p => p.Addresses) + .HasForeignKey(p => p.Employee_Id) + .OnDelete(DeleteBehavior.Cascade); + + builder.Property(p => p.Country).IsRequired(); + builder.Property(p => p.Street).IsRequired(); + builder.Property(p => p.CreatedBy).IsRequired(); + builder.Property(p => p.CreatedAt).IsRequired(); + builder.Property(p => p.ModifiedBy).IsRequired(false); + builder.Property(p => p.ModifiedAt).IsRequired(false); + builder.Property(p => p.Remark).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs new file mode 100644 index 00000000..e9e2f6d9 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class Identity + { + public Guid Employee_Id { get; set; } + + // [ForeignKey("Employee_Id")] + public virtual Employee Employee { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Remark { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string? ModifiedBy { get; set; } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs new file mode 100644 index 00000000..b115d8a2 --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs @@ -0,0 +1,15 @@ +namespace Lab.ChangeTracking.Infrastructure.DB; + +public class EnvironmentAssistant +{ + public static string GetEnvironmentVariable(string key) + { + var result = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrWhiteSpace(result)) + { + throw new Exception($"the key '{key}' not exist in environment variable"); + } + + return result; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj new file mode 100644 index 00000000..d944728b --- /dev/null +++ b/Property Change Tracking/ChangeTracking2/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTrackingForORM/ChangeTrackingForORM.sln b/Property Change Tracking/ChangeTrackingForORM/ChangeTrackingForORM.sln new file mode 100644 index 00000000..783f6db4 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/ChangeTrackingForORM.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{1F776B7D-C182-4DC3-BA57-844657BD9AC0}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + Makefile = Makefile + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{483A9EB7-C4AF-4D68-80ED-49277985C760}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Infrastructure.DB", "src\Lab.ChangeTracking.Infrastructure.DB\Lab.ChangeTracking.Infrastructure.DB.csproj", "{A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Domain", "src\Lab.ChangeTracking.Domain\Lab.ChangeTracking.Domain.csproj", "{A1C827F2-683E-470C-A3DE-BD6DD2BE5198}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Domain.UnitTest", "src\Lab.ChangeTracking.Domain.UnitTest\Lab.ChangeTracking.Domain.UnitTest.csproj", "{7066EE2C-28A8-4408-B212-E582608AB967}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ChangeTracking.Abstract", "src\Lab.ChangeTracking.Abstract\Lab.ChangeTracking.Abstract.csproj", "{28D1EF37-8DEA-47BD-83A1-4302CC590791}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C}.Release|Any CPU.Build.0 = Release|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198}.Release|Any CPU.Build.0 = Release|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7066EE2C-28A8-4408-B212-E582608AB967}.Release|Any CPU.Build.0 = Release|Any CPU + {28D1EF37-8DEA-47BD-83A1-4302CC590791}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28D1EF37-8DEA-47BD-83A1-4302CC590791}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28D1EF37-8DEA-47BD-83A1-4302CC590791}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28D1EF37-8DEA-47BD-83A1-4302CC590791}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A2FDDEA2-F3BD-43B2-96FE-B12EA5BE909C} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {A1C827F2-683E-470C-A3DE-BD6DD2BE5198} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {7066EE2C-28A8-4408-B212-E582608AB967} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {28D1EF37-8DEA-47BD-83A1-4302CC590791} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + EndGlobalSection +EndGlobal diff --git a/Property Change Tracking/ChangeTrackingForORM/Makefile b/Property Change Tracking/ChangeTrackingForORM/Makefile new file mode 100644 index 00000000..cf17f07f --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/Makefile @@ -0,0 +1,4 @@ +G1: + echo 'Hello World' +G2: + cmd \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/docker-compose.yml b/Property Change Tracking/ChangeTrackingForORM/docker-compose.yml new file mode 100644 index 00000000..8ecb1567 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.8" + +services: + db-sql: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=pass@w0rd1~ + ports: + - 1433:1433 \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Employee/IEmployeeEntity.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Employee/IEmployeeEntity.cs new file mode 100644 index 00000000..5d91da43 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Employee/IEmployeeEntity.cs @@ -0,0 +1,17 @@ +namespace Lab.ChangeTracking.Abstract; + +public interface IEmployeeEntity : IChangeTrackable +{ + public Guid Id { get; set; } + + public string Name { get; set; } + + public int? Age { get; set; } + + public string Remark { get; set; } + + // public IList Profiles { get; set; } + // + // public IIdentityEntity Identity { get; set; } + +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Employee/IIdentityEntity.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Employee/IIdentityEntity.cs new file mode 100644 index 00000000..01defb52 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Employee/IIdentityEntity.cs @@ -0,0 +1,10 @@ +namespace Lab.ChangeTracking.Abstract; + +public interface IIdentityEntity : IChangeTime +{ + public string Account { get; set; } + + public string Password { get; set; } + + public string Remark { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Employee/IProfileEntity.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Employee/IProfileEntity.cs new file mode 100644 index 00000000..f0acfa1d --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Employee/IProfileEntity.cs @@ -0,0 +1,10 @@ +namespace Lab.ChangeTracking.Abstract; + +public interface IProfileEntity : IChangeTime +{ + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Remark { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/IChangeState.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/IChangeState.cs new file mode 100644 index 00000000..c906d4ef --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/IChangeState.cs @@ -0,0 +1,6 @@ +namespace Lab.ChangeTracking.Abstract; + +public interface IChangeState +{ + int Version { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/IChangeTime.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/IChangeTime.cs new file mode 100644 index 00000000..2bb10097 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/IChangeTime.cs @@ -0,0 +1,12 @@ +namespace Lab.ChangeTracking.Abstract; + +public interface IChangeTime +{ + DateTimeOffset CreatedAt { get; set; } + + string CreatedBy { get; set; } + + DateTimeOffset UpdatedAt { get; set; } + + string UpdatedBy { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/IChangeTrackable.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/IChangeTrackable.cs new file mode 100644 index 00000000..24c9736f --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/IChangeTrackable.cs @@ -0,0 +1,5 @@ +namespace Lab.ChangeTracking.Abstract; + +public interface IChangeTrackable : IChangeTime, IChangeState +{ +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Lab.ChangeTracking.Abstract.csproj b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Lab.ChangeTracking.Abstract.csproj new file mode 100644 index 00000000..eb2460e9 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Abstract/Lab.ChangeTracking.Abstract.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs new file mode 100644 index 00000000..78689c66 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/ChangeTrackingUnitTest.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Unicode; +using System.Threading.Tasks; +using Lab.ChangeTracking.Abstract; +using Lab.ChangeTracking.Domain.EmployeeAggregate; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Lab.MultiTestCase.UnitTest; +using Microsoft.EntityFrameworkCore; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; + +namespace Lab.ChangeTracking.Domain.UnitTest; + +[TestClass] +public class ChangeTrackingUnitTest +{ + private readonly IEmployeeAggregate _employeeAggregate = TestAssistants.EmployeeAggregate; + + private readonly IDbContextFactory _employeeDbContextFactory = + TestAssistants.EmployeeDbContextFactory; + + [TestMethod] + public async Task 新增() + { + // arrange + var employeeRepository = TestAssistants.EmployeeRepository; + var systemClock = Substitute.For(); + systemClock.Now.Returns(DateTimeOffset.Parse("2021-01-01")); + var uuIdProvider = Substitute.For(); + uuIdProvider.GenerateId().Returns(TestAssistants.Parse("1")); + var accessContext = Substitute.For(); + accessContext.AccessNow.Returns(DateTimeOffset.Parse("2021-01-02")); + accessContext.AccessUserId.Returns("System User"); + + // act + var employeeAggregate = + new EmployeeAggregate.EmployeeAggregate(employeeRepository, uuIdProvider, systemClock, accessContext); + employeeAggregate.Initial("yao", 18, "Test User"); + employeeAggregate.SubmitChange(); + var result = await employeeAggregate.SaveChangeAsync(); + + // assert + await using var dbContext = await this._employeeDbContextFactory.CreateDbContextAsync(); + var actual = dbContext.Employees.AsTracking().FirstOrDefault(); + Assert.AreEqual("yao", actual.Name); + Assert.AreEqual(18, actual.Age); + Assert.AreEqual(1, actual.Version); + Assert.AreEqual("Test User", actual.Remark); + } + + [TestMethod] + public async Task 編輯() + { + // arrange + InsertTestData(); + var employeeRepository = TestAssistants.EmployeeRepository; + var systemClock = Substitute.For(); + systemClock.Now.Returns(DateTimeOffset.Parse("2021-01-02")); + var uuIdProvider = Substitute.For(); + uuIdProvider.GenerateId().Returns(TestAssistants.Parse("1")); + var accessContext = Substitute.For(); + accessContext.AccessNow.Returns(DateTimeOffset.Parse("2021-01-02")); + + // act + var employeeAggregate = + new EmployeeAggregate.EmployeeAggregate(employeeRepository, uuIdProvider, systemClock, accessContext); + await employeeAggregate.GetAsync(TestAssistants.Parse("1")); + employeeAggregate.SetName("小章").SetAge(20); + employeeAggregate.SubmitChange(); + var count = await employeeAggregate.SaveChangeAsync(); + + // assert + await using var dbContext = await this._employeeDbContextFactory.CreateDbContextAsync(); + var actual = dbContext.Employees.AsTracking().FirstOrDefault(); + Assert.AreEqual("小章", actual.Name); + Assert.AreEqual(20, actual.Age); + Assert.AreEqual(2, actual.Version); + } + [TestMethod] + public async Task 編輯1() + { + // arrange + InsertTestData(); + await using var dbContext = await TestAssistants.EmployeeDbContextFactory.CreateDbContextAsync(); + + var employee = dbContext.Employees + .Include(p => p.Profiles) + .AsTracking() + // .Load() + .FirstOrDefault() + ; + var now = DateTimeOffset.Now; + var accessUserId = "TEST USER"; + var newProfile = new Profile + { + Id = Guid.NewGuid(), + Employee_Id = employee.Id, + CreatedAt = now, + CreatedBy = accessUserId, + UpdatedAt = now, + UpdatedBy = accessUserId, + FirstName = "first name", + LastName = "last name", + }; + employee.Profiles.Add(newProfile); + // dbContext.Profiles.Add(newProfile); + await dbContext.SaveChangesAsync(); + } + + private static Employee InsertTestData() + { + Console.WriteLine("新增資料"); + using var dbContext = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + var employeeId = TestAssistants.Parse("1"); + var now = DateTimeOffset.Now; + var accessUserId = "TEST USER"; + var toDB = new Employee + { + Id = employeeId, + Age = 18, + Name = "yao", + CreatedAt = now, + CreatedBy = accessUserId, + Identity = new() + { + Employee_Id = employeeId, + Account = "yao", + Password = "123456", + CreatedAt = now, + CreatedBy = accessUserId, + UpdatedAt = now, + UpdatedBy = accessUserId, + }, + Version = 1, + Profiles = new List() + { + new() + { + Id = Guid.NewGuid(), + Employee_Id = employeeId, + CreatedAt = now, + CreatedBy = accessUserId, + UpdatedAt = now, + UpdatedBy = accessUserId, + FirstName = "yao", + LastName = "yu", + } + }, + }; + dbContext.Employees.Add(toDB); + dbContext.SaveChanges(); + + return toDB; + } + + private static string ToJson(T instance) + { + var serialize = JsonSerializer.Serialize(instance, + new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.Create( + UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs) + }); + return serialize; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj new file mode 100644 index 00000000..b29d606e --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/Lab.ChangeTracking.Domain.UnitTest.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs new file mode 100644 index 00000000..ca75b47b --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/MsTestHook.cs @@ -0,0 +1,31 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.MultiTestCase.UnitTest; + +[TestClass] +public class MsTestHook +{ + [AssemblyCleanup] + public static void Cleanup() + { + TestAssistants.SetTestEnvironmentVariable(); + var db = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + } + + [AssemblyInitialize] + public static void Setup(TestContext context) + { + TestAssistants.SetTestEnvironmentVariable(); + var db = TestAssistants.EmployeeDbContextFactory.CreateDbContext(); + if (db.Database.CanConnect()) + { + db.Database.EnsureDeleted(); + } + + db.Database.EnsureCreated(); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs new file mode 100644 index 00000000..1c502ed8 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain.UnitTest/TestAssistants.cs @@ -0,0 +1,58 @@ +using System; +using Lab.ChangeTracking.Abstract; +using Lab.ChangeTracking.Domain; +using Lab.ChangeTracking.Domain.EmployeeAggregate; +using Lab.ChangeTracking.Domain.EmployeeAggregate.Repository; +using Lab.ChangeTracking.Infrastructure.DB; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Lab.MultiTestCase.UnitTest; + +// assistant +internal class TestAssistants +{ + private static IServiceProvider _serviceProvider; + + public static IDbContextFactory EmployeeDbContextFactory => + _serviceProvider.GetService>(); + + public static IEmployeeRepository EmployeeRepository => + _serviceProvider.GetService(); + + public static IEmployeeAggregate EmployeeAggregate => + _serviceProvider.GetService>(); + + static TestAssistants() + { + var services = new ServiceCollection(); + ConfigureTestServices(services); + SetTestEnvironmentVariable(); + } + + public static void ConfigureTestServices(IServiceCollection services) + { + services.AddAppEnvironment(); + services.AddEntityFramework(); + + services.AddSingleton(); + services.AddSingleton(); + // services.AddSingleton(); + _serviceProvider = services.BuildServiceProvider(); + } + + public static void SetTestEnvironmentVariable() + { + var option = _serviceProvider.GetService(); + option.EmployeeDbConnectionString = + "Data Source=localhost;Initial Catalog=EmployeeDb;Integrated Security=false;User ID=sa;Password=pass@w0rd1~;MultipleActiveResultSets=True;TrustServerCertificate=True"; + } + public static Guid Parse(string id) + { + var guidFormat = "{0}-0000-0000-0000-000000000000"; + var guidText = string.Format(guidFormat, id.PadRight(8, '0')); + var key = Guid.Parse(guidText); + return key; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/AccessContext.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/AccessContext.cs new file mode 100644 index 00000000..fb42dfc6 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/AccessContext.cs @@ -0,0 +1,8 @@ +namespace Lab.ChangeTracking.Domain; + +public class AccessContext : IAccessContext +{ + public string AccessUserId { get; set; } + + public DateTimeOffset AccessNow { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/AggregationRoot.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/AggregationRoot.cs new file mode 100644 index 00000000..6e115ba5 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/AggregationRoot.cs @@ -0,0 +1,132 @@ +using Lab.ChangeTracking.Abstract; + +namespace Lab.ChangeTracking.Domain; + +public abstract class AggregationRoot : IAggregationRoot where T : IChangeTrackable +{ + public EntityState State { get; protected set; } + + /// + /// 建立時間 + /// + public DateTimeOffset CreatedAt + { + get => this._instance.CreatedAt; + init => this._instance.CreatedAt = value; + } + + /// + /// 建立者 + /// + public string CreatedBy + { + get => this._instance.CreatedBy; + init => this._instance.CreatedBy = value; + } + + /// + /// 異動時間 + /// + public DateTimeOffset UpdatedAt + { + get => this._instance.UpdatedAt; + init => this._instance.UpdatedAt = value; + } + + /// + /// 異動者 + /// + public string UpdatedBy + { + get => this._instance.UpdatedBy; + init => this._instance.UpdatedBy = value; + } + + /// + /// 異動版號 + /// + public int Version + { + get => this._instance.Version; + init => this._instance.Version = value; + } + + private readonly IList> _changeActions = new List>(); + protected readonly Dictionary ChangedProperties = new(); + protected readonly Dictionary OriginalValues = new(); + protected IUUIdProvider _uuIdProvider; + protected T _instance; + protected ISystemClock _systemClock; + protected IAccessContext _accessContext; + + public IReadOnlyList> GetChangeActions() + { + return this._changeActions.ToList(); + } + + public void SetInstance(T instance) + { + this._instance = instance; + this.State = EntityState.Unchanged; + } + + /// + /// SubmitChange 後則進版號 + /// + /// + public (Error err, bool changed) SubmitChange() + { + var (now,accessUserId )= (this._accessContext.AccessNow,this._accessContext.AccessUserId); + if (this.State == EntityState.Submitted) + { + return ( + new Error("STATE_CONFLICT", + $"Entity({this.State}) was submitted and should not submit again."), false); + } + + if (this.State == EntityState.Unchanged) + { + return (null, false); + } + + if (this.State == EntityState.Added) + { + this.ChangeTrack(x => x.CreatedAt = now); + this.ChangeTrack(x => x.CreatedBy = accessUserId); + this.ChangeTrack(x => x.UpdatedAt = now); + this.ChangeTrack(x => x.UpdatedBy = accessUserId); + this.ChangeTrack(x => x.Version += 1); + } + else + { + this.ChangeTrack(x => x.UpdatedAt = now); + this.ChangeTrack(x => x.UpdatedBy = accessUserId); + this.ChangeTrack(x => x.Version += 1); + } + + this.State = EntityState.Submitted; + + return (null, true); + } + + public void ChangeTrack(Action changeAction) + { + if (this.State == EntityState.Submitted) + { + throw new Exception("已經 Submitted 的 Doamin,無法再進行修改。"); + } + + changeAction(this._instance); + this._changeActions.Add(changeAction); + + if (this.State == EntityState.Unchanged) + { + this.State = EntityState.Modified; + } + } + + private T Clone(T source) + { + return (T)Activator.CreateInstance(typeof(T)); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs new file mode 100644 index 00000000..85d15e12 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/EmployeeAggregate.cs @@ -0,0 +1,78 @@ +using Lab.ChangeTracking.Abstract; +using Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; +using Lab.ChangeTracking.Domain.EmployeeAggregate.Repository; + +namespace Lab.ChangeTracking.Domain.EmployeeAggregate; + +// public class EmployeeAggregate : AggregationRoot, +// IEmployeeAggregate +public class EmployeeAggregate : AggregationRoot +{ + public Guid Id => this._instance.Id; + + public string Name => this._instance.Name; + + public int? Age => this._instance.Age; + + public string Remark => this._instance.Remark; + + private readonly IEmployeeRepository _repository; + + public EmployeeAggregate(IEmployeeRepository repository, + IUUIdProvider uuIdProvider, + ISystemClock systemClock, + IAccessContext accessContext) + { + this._repository = repository; + this._uuIdProvider = uuIdProvider; + this._accessContext = accessContext; + this._systemClock = systemClock; + } + + public async Task GetAsync(Guid id, CancellationToken cancel = default) + { + this._instance = await this._repository.GetAsync(id, cancel); + + this.State = EntityState.Unchanged; + } + + public void Initial(string name, int age, string remark = null) + { + this._instance = new EmployeeEntity(); + + this.ChangeTrack(p => p.Id = this._uuIdProvider.GenerateId()); + this.ChangeTrack(p => p.Age = age); + this.ChangeTrack(p => p.Name = name); + this.ChangeTrack(p => p.Version = 0); + this.ChangeTrack(p => p.Remark = remark); + this.State = EntityState.Added; + } + + public async Task SaveChangeAsync(CancellationToken cancel = default) + { + return await this._repository.SaveChangeAsync(this, cancel); + } + + public EmployeeAggregate SetAge(int age) + { + var instance = this._instance; + if (instance.Age != age) + { + this.ChangeTrack(p => p.Age = age); + } + + return this; + } + + // public IEmployeeAggregate SetName(string name) + public EmployeeAggregate SetName(string name) + { + var instance = this._instance; + if (instance.Name != name) + { + this.ChangeTrack(p => p.Name = name); + } + + return this; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs new file mode 100644 index 00000000..4b0f32ae --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/EmployeeEntity.cs @@ -0,0 +1,28 @@ +using Lab.ChangeTracking.Abstract; + +namespace Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; + +public record EmployeeEntity : IEmployeeEntity +{ + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset UpdatedAt { get; set; } + + public string UpdatedBy { get; set; } + + public int Version { get; set; } + + public Guid Id { get; set; } + + public string Name { get; set; } + + public int? Age { get; set; } + + public string Remark { get; set; } + + // public IList Profiles { get; set; } + + public IdentityEntity Identity { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs new file mode 100644 index 00000000..09505d0f --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/IdentityEntity.cs @@ -0,0 +1,14 @@ +namespace Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; + +public record IdentityEntity +{ + public virtual string Account { get; set; } + + public virtual string Password { get; set; } + + public virtual string Remark { get; set; } + + public virtual DateTimeOffset CreateAt { get; set; } + + public virtual string CreateBy { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/ProfileEntity.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/ProfileEntity.cs new file mode 100644 index 00000000..9ae35128 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Entity/ProfileEntity.cs @@ -0,0 +1,8 @@ +namespace Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; + +public record ProfileEntity +{ + public virtual string FirstName { get; set; } + + public virtual string LastName { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs new file mode 100644 index 00000000..c86d7c76 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/IEmployeeAggregate.cs @@ -0,0 +1,12 @@ +using Lab.ChangeTracking.Abstract; + +namespace Lab.ChangeTracking.Domain.EmployeeAggregate; + +public interface IEmployeeAggregate : IAggregationRoot where T : IChangeTrackable +{ + IEmployeeAggregate SetName(string name); + + IEmployeeAggregate SetAge(int age); + + void SaveChangeAsync(CancellationToken cancel = default); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs new file mode 100644 index 00000000..29f3515a --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/EmployeeRepository.cs @@ -0,0 +1,101 @@ +using Lab.ChangeTracking.Abstract; +using Lab.ChangeTracking.Domain.EmployeeAggregate.Entity; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; + +namespace Lab.ChangeTracking.Domain.EmployeeAggregate.Repository; + +public class EmployeeRepository : IEmployeeRepository +{ + private readonly IDbContextFactory _employeeContextFactory; + + public EmployeeRepository(IDbContextFactory employeeContextFactory) + { + this._employeeContextFactory = employeeContextFactory; + } + + public Employee To(EmployeeEntity srcEmployee) + { + return new Employee + { + Id = srcEmployee.Id, + Name = srcEmployee.Name, + Age = srcEmployee.Age, + Remark = srcEmployee.Remark, + CreatedAt = srcEmployee.CreatedAt, + CreatedBy = srcEmployee.CreatedBy, + Identity = this.To(srcEmployee.Identity) + }; + } + + public Identity To(IdentityEntity srcIdentity) + { + return new Identity + { + Password = srcIdentity.Password, + Remark = srcIdentity.Remark, + CreatedAt = srcIdentity.CreateAt, + CreatedBy = srcIdentity.CreateBy, + }; + } + + public async Task SaveChangeAsync(IEmployeeAggregate srcEmployee, + CancellationToken cancel = default) + { + // var employeeEntity = srcEmployee.GetInstance(); + var employee = new Employee(); + foreach (var change in srcEmployee.GetChangeActions()) + { + change(employee); + } + + await using var dbContext = await this._employeeContextFactory.CreateDbContextAsync(cancel); + return 1; + } + + public async Task SaveChangeAsync(EmployeeAggregate srcEmployee, CancellationToken cancel = default) + { + await using var dbContext = await this._employeeContextFactory.CreateDbContextAsync(cancel); + + if (srcEmployee.State != EntityState.Submitted) + { + throw new Exception($"尚未 {nameof(EntityState.Submitted)},不能存檔"); + } + + var employeeFromDb = await dbContext.Employees + .FirstOrDefaultAsync(x => x.Id == srcEmployee.Id, cancel); + if (employeeFromDb == null) + { + var toDb = new Employee(); + foreach (var changeAction in srcEmployee.GetChangeActions()) + { + changeAction(toDb); + } + + dbContext.Add(toDb); + } + else + { + foreach (var changeAction in srcEmployee.GetChangeActions()) + { + changeAction(employeeFromDb); + } + } + + return await dbContext.SaveChangesAsync(cancel); + } + + public Task SaveChangeAsync(IEmployeeEntity employee, CancellationToken cancel = default) + { + throw new NotImplementedException(); + } + + public async Task GetAsync(Guid id, CancellationToken cancel = default) + { + await using var dbContext = await this._employeeContextFactory.CreateDbContextAsync(cancel); + return await dbContext.Employees + .Where(p => p.Id == id) + .AsNoTracking() + .FirstOrDefaultAsync(cancel); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs new file mode 100644 index 00000000..2ebb4645 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs @@ -0,0 +1,12 @@ +using Lab.ChangeTracking.Abstract; + +namespace Lab.ChangeTracking.Domain.EmployeeAggregate.Repository; + +public interface IEmployeeRepository +{ + // Task SaveChangeAsync(IEmployeeAggregate employee, CancellationToken cancel = default); + Task SaveChangeAsync(EmployeeAggregate employee, CancellationToken cancel = default); + Task SaveChangeAsync(IEmployeeEntity employee, CancellationToken cancel = default); + + Task GetAsync(Guid id, CancellationToken cancel); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeRepositoryExtensions.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeRepositoryExtensions.cs new file mode 100644 index 00000000..3810dc0e --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EmployeeRepositoryExtensions.cs @@ -0,0 +1,22 @@ +using Lab.ChangeTracking.Abstract; +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; + +namespace Lab.ChangeTracking.Domain; + +static class EmployeeRepositoryExtensions +{ + public static Employee ToDataEntity(this IEmployeeEntity srcEmployee) + { + return new Employee + { + Id = srcEmployee.Id, + Name = srcEmployee.Name, + Age = srcEmployee.Age, + Remark = srcEmployee.Remark, + CreatedAt = srcEmployee.CreatedAt, + CreatedBy = srcEmployee.CreatedBy, + // Identity = this.To(srcEmployee.Identity) + }; + } + +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EntityState.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EntityState.cs new file mode 100644 index 00000000..7b9757ca --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/EntityState.cs @@ -0,0 +1,9 @@ +namespace Lab.ChangeTracking.Domain; + +public enum EntityState +{ + Added = 0, + Modified = 1, + Submitted = 2, + Unchanged = 99, +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/Error.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/Error.cs new file mode 100644 index 00000000..7dbcb965 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/Error.cs @@ -0,0 +1,2 @@ +namespace Lab.ChangeTracking.Domain; +public record Error(T Code, object Message); \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/IAccessContext.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/IAccessContext.cs new file mode 100644 index 00000000..73dbed6c --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/IAccessContext.cs @@ -0,0 +1,8 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IAccessContext +{ + string AccessUserId { get; set; } + + DateTimeOffset AccessNow { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/IAggregationRoot.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/IAggregationRoot.cs new file mode 100644 index 00000000..2a5c4ade --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/IAggregationRoot.cs @@ -0,0 +1,18 @@ +using Lab.ChangeTracking.Abstract; + +namespace Lab.ChangeTracking.Domain; + +public interface IAggregationRoot where T : IChangeTrackable +{ + IReadOnlyList> GetChangeActions(); + + void SetInstance(T instance); + + /// + /// SubmitChange 後則進版號 + /// + /// + (Error err, bool changed) SubmitChange(); + + void ChangeTrack(Action change); +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/ISystemClock.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/ISystemClock.cs new file mode 100644 index 00000000..945faa76 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/ISystemClock.cs @@ -0,0 +1,11 @@ +namespace Lab.ChangeTracking.Domain; + +public interface ISystemClock +{ + DateTimeOffset Now { get; set; } +} + +public class SystemClock : ISystemClock +{ + public DateTimeOffset Now { get; set; }=DateTimeOffset.Now; +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/IUUIDProvider.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/IUUIDProvider.cs new file mode 100644 index 00000000..a58ce071 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/IUUIDProvider.cs @@ -0,0 +1,14 @@ +namespace Lab.ChangeTracking.Domain; + +public interface IUUIdProvider +{ + Guid GenerateId(); +} + +public class UUIdProvider : IUUIdProvider +{ + public Guid GenerateId() + { + return Guid.NewGuid(); + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj new file mode 100644 index 00000000..def7c560 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Domain/Lab.ChangeTracking.Domain.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs new file mode 100644 index 00000000..6fc51c5c --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/AppDependencyInjectionExtensions.cs @@ -0,0 +1,50 @@ +using Lab.ChangeTracking.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.ChangeTracking.Infrastructure.DB; + +public static class AppDependencyInjectionExtensions +{ + public static void AddAppEnvironment(this IServiceCollection services) + { + services.AddLogging(builder => + { + builder.AddConsole(); + }); + services.AddSingleton(); + } + + public static void AddEntityFramework(this IServiceCollection services) + { + services.AddPooledDbContextFactory((provider, optionsBuilder) => + { + var option = provider.GetService(); + var connectionString = option.EmployeeDbConnectionString; + var loggerFactory = provider.GetService(); + optionsBuilder.UseSqlServer(connectionString) + .UseLoggerFactory(loggerFactory) + ; + }); + + // services.AddPooledDbContextFactory((provider, options) => + // { + // var option = provider.GetService(); + // var loggerFactory = provider.GetService(); + // options.UseNpgsql( + // option.MemberDbConnectionString, //只會呼叫一次 + // builder => + // builder.EnableRetryOnFailure( + // 10, + // TimeSpan.FromSeconds(30), + // new List { "57P01" })) + // + // // .UseLazyLoadingProxies() + // .UseSnakeCaseNamingConvention() + // .UseLoggerFactory(loggerFactory) + // ; + // }); + } + +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs new file mode 100644 index 00000000..e29f53f3 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/AppEnvironmentOption.cs @@ -0,0 +1,31 @@ +namespace Lab.ChangeTracking.Infrastructure.DB; + +public class AppEnvironmentOption +{ + public string EmployeeDbConnectionString + { + get + { + if (string.IsNullOrWhiteSpace(this._employeeDbConnectionString)) + { + this._employeeDbConnectionString = + EnvironmentAssistant.GetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR); + } + + return this._employeeDbConnectionString; + } + set + { + this._employeeDbConnectionString = value; + Environment.SetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR, value); + } + } + + private string _employeeDbConnectionString; + private readonly string EMPLOYEE_DB_CONN_STR = "EMPLOYEE_DB_CONNECTION_STR"; + + public void Initial() + { + var memberDbConnectionString = this.EmployeeDbConnectionString; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs new file mode 100644 index 00000000..49094f01 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Employee.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Lab.ChangeTracking.Abstract; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class Employee : IEmployeeEntity + { + public Guid Id { get; set; } + + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + public string Remark { get; set; } + + public List Profiles { get; set; } = new(); + + public Identity Identity { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset UpdatedAt { get; set; } + + public string UpdatedBy { get; set; } + + public int Version { get; set; } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..03ec4140 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,114 @@ +using System.Dynamic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.InMemory.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = { false }; + + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identities { get; set; } + + public virtual DbSet Profiles { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + // 改用 CI 執行 Migrate + + // if (s_migrated[0]) + // { + // return; + // } + // + // lock (s_migrated) + // { + // if (s_migrated[0] == false) + // { + // var sqlOptions = options.FindExtension(); + // if (sqlOptions != null) + // { + // this.Database.Migrate(); + // } + // + // s_migrated[0] = true; + // } + // } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new EmployeeConfiguration()); + modelBuilder.ApplyConfiguration(new IdentityConfiguration()); + modelBuilder.ApplyConfiguration(new ProfileConfiguration()); + } + + internal class EmployeeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Employee"); + builder.HasKey(p => p.Id) + .IsClustered(false); + + builder.Property(p => p.Name).IsRequired(true); + builder.Property(p => p.Remark).IsRequired(false); + builder.Property(p => p.UpdatedBy).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + + private class IdentityConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Identity"); + builder.HasKey(p => p.Employee_Id).IsClustered(false); + builder.HasOne(e => e.Employee) + .WithOne(p => p.Identity) + .HasForeignKey(p => p.Employee_Id) + .OnDelete(DeleteBehavior.Cascade) + ; + + builder.Property(p => p.Account).IsRequired(); + builder.Property(p => p.Password).IsRequired(); + builder.Property(p => p.Remark).IsRequired(false); + builder.Property(p => p.UpdatedBy).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + + private class ProfileConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Profile"); + builder.HasKey(p => p.Id).IsClustered(false); + builder.HasOne(e => e.Employee) + .WithMany(p => p.Profiles) + .HasForeignKey(p => p.Employee_Id) + .OnDelete(DeleteBehavior.Cascade) ; + + builder.Property(p => p.FirstName).IsRequired(); + builder.Property(p => p.LastName).IsRequired(); + builder.Property(p => p.Remark).IsRequired(false); + builder.Property(p => p.UpdatedBy).IsRequired(false); + + builder.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + } + } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs new file mode 100644 index 00000000..62b36ba3 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Identity.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Security.Principal; +using Lab.ChangeTracking.Abstract; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel +{ + public class Identity : IIdentityEntity + { + public Guid Employee_Id { get; set; } + + // [ForeignKey("Employee_Id")] + public virtual Employee Employee { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Remark { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset UpdatedAt { get; set; } + + public string UpdatedBy { get; set; } + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Profile.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Profile.cs new file mode 100644 index 00000000..140c8f05 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EntityModel/Profile.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Lab.ChangeTracking.Abstract; + +namespace Lab.ChangeTracking.Infrastructure.DB.EntityModel; + +public class Profile : IProfileEntity +{ + public Guid Id { get; set; } + + public Guid Employee_Id { get; set; } + + public Employee Employee { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset UpdatedAt { get; set; } + + public string UpdatedBy { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Remark { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int SequenceId { get; set; } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs new file mode 100644 index 00000000..b115d8a2 --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/EnvironmentAssistant.cs @@ -0,0 +1,15 @@ +namespace Lab.ChangeTracking.Infrastructure.DB; + +public class EnvironmentAssistant +{ + public static string GetEnvironmentVariable(string key) + { + var result = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrWhiteSpace(result)) + { + throw new Exception($"the key '{key}' not exist in environment variable"); + } + + return result; + } +} \ No newline at end of file diff --git a/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj new file mode 100644 index 00000000..d944728b --- /dev/null +++ b/Property Change Tracking/ChangeTrackingForORM/src/Lab.ChangeTracking.Infrastructure.DB/Lab.ChangeTracking.Infrastructure.DB.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 00000000..af9e31ca --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# sample.dotblog +https://dotblogs.com.tw/yc421206/ +範例程式 diff --git a/StructLog/Lab.SerilogProject/.gitignore b/StructLog/Lab.SerilogProject/.gitignore new file mode 100644 index 00000000..81c554f7 --- /dev/null +++ b/StructLog/Lab.SerilogProject/.gitignore @@ -0,0 +1,350 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +#vistual studio extension +.localhistory + +# stress test output +stress/output + +# specflow feature.cs +**/*.feature.cs + +# secrets +secrets + +.DS_Store +*.zip + +deployments + +# minio local s3 +minio \ No newline at end of file diff --git a/StructLog/Lab.SerilogProject/Taskfile.yml b/StructLog/Lab.SerilogProject/Taskfile.yml new file mode 100644 index 00000000..e5b8cef3 --- /dev/null +++ b/StructLog/Lab.SerilogProject/Taskfile.yml @@ -0,0 +1,22 @@ +# Taskfile.yml + +version: "3" + +dotenv: [ "secrets/secrets.env" ] + +tasks: + webapi: + desc: WebApi Development + dir: "src/Lab.SerilogProject.WebApi" + cmds: + - dotnet run --environment Staging + app: + desc: WebApi Development + dir: "src/Lab.SerilogProject.ConsoleApp" + cmds: + - dotnet run --environment Production + + seq-start: + desc: start seq service + cmds: + - docker run --name seq -d --restart unless-stopped -e ACCEPT_EULA=Y -p 5341:80 datalust/seq:latest \ No newline at end of file diff --git a/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Controllers/WeatherForecastController.cs b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..7bb79242 --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.SerilogProject.WebApi.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateTime.Now.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} \ No newline at end of file diff --git a/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Lab.SerilogProject.WebApi.csproj b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Lab.SerilogProject.WebApi.csproj new file mode 100644 index 00000000..458e3800 --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Lab.SerilogProject.WebApi.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + diff --git a/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Program.cs b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Program.cs new file mode 100644 index 00000000..bebb259c --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Program.cs @@ -0,0 +1,61 @@ +using Serilog; +using Serilog.Events; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/app-.txt", rollingInterval: RollingInterval.Day) + .CreateLogger(); +try +{ + Log.Information("Starting web host"); + + var builder = WebApplication.CreateBuilder(args); + + // builder.Host.UseSerilog(); //<=== 讓 Host 使用 Serilog + builder.Host.UseSerilog((context, services, config) => + config.ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.Seq("http://localhost:5341") + // .WriteTo.File("logs/aspnet-.txt", rollingInterval: RollingInterval.Minute) + ); + + // Add services to the container. + + builder.Services.AddControllers(); + + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseHttpsRedirection(); + app.UseSerilogRequestLogging(); //<=== 每一個 Request 使用 Serilog 記錄下來 + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); + return 0; +} +catch (Exception ex) +{ + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} \ No newline at end of file diff --git a/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/WeatherForecast.cs b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/WeatherForecast.cs new file mode 100644 index 00000000..6bf80410 --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Lab.SerilogProject.WebApi; + +public class WeatherForecast +{ + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} \ No newline at end of file diff --git a/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/appsettings.Development.json b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/appsettings.json b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.sln b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.sln new file mode 100644 index 00000000..263368a0 --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.SerilogProject.WebApi", "Lab.SerilogProject.WebApi\Lab.SerilogProject.WebApi.csproj", "{D58F5F77-C34C-4971-B044-565BED8C9A5D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{57242756-8814-45F1-A9E3-474721C382BC}" + ProjectSection(SolutionItems) = preProject + ..\..\.gitignore = ..\..\.gitignore + ..\..\Taskfile.yml = ..\..\Taskfile.yml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D58F5F77-C34C-4971-B044-565BED8C9A5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D58F5F77-C34C-4971-B044-565BED8C9A5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D58F5F77-C34C-4971-B044-565BED8C9A5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D58F5F77-C34C-4971-B044-565BED8C9A5D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Template/Lab.RazorTemplate/Lab.RazorTemplate.Test/K8sTemplateEngineTests.cs b/Template/Lab.RazorTemplate/Lab.RazorTemplate.Test/K8sTemplateEngineTests.cs new file mode 100644 index 00000000..d8b241c1 --- /dev/null +++ b/Template/Lab.RazorTemplate/Lab.RazorTemplate.Test/K8sTemplateEngineTests.cs @@ -0,0 +1,49 @@ +using System.Net; +using System.Text; + +namespace Lab.RazorTemplate.Test; + +[TestClass] +public class K8sTemplateEngineTests +{ + [TestMethod] + public async Task 替換範本() + { + var templatePath = "Template.ConfigMap.cshtml"; + var k8sValue = new K8sValue() + { + Common = new Common + { + ProjectName = "member-service-api", + Namespace = "member-service", + }, + }; + var k8sDynamicValues = new Dictionary + { + ["Value1"] = "1", + ["Value2"] = "2", + ["K8S_COMMON_SERVICE_NAME"] = "3", + }; + + var engine = new K8sTemplateEngine(); + var result = await engine.RenderAsync(templatePath, k8sValue, k8sDynamicValues); + Console.WriteLine($"Render Result:\r\n{result}"); + } + + [TestMethod] + public async Task 替換範本_1() + { + var templatePath = "EnvTemplate.cshtml"; + var k8sValue = new K8sValue(); + var k8sDynamicValues = new Dictionary + { + ["Market"] = "TW", + ["Environment"] = "Dev", + }; + + var engine = new K8sTemplateEngine(); + var result = await engine.RenderAsync(templatePath, k8sValue, k8sDynamicValues); + Console.WriteLine(); + Console.WriteLine($"Render Result:\r\n{result}"); + } +} \ No newline at end of file diff --git a/Template/Lab.RazorTemplate/Lab.RazorTemplate.Test/Lab.RazorTemplate.Test.csproj b/Template/Lab.RazorTemplate/Lab.RazorTemplate.Test/Lab.RazorTemplate.Test.csproj new file mode 100644 index 00000000..54419e07 --- /dev/null +++ b/Template/Lab.RazorTemplate/Lab.RazorTemplate.Test/Lab.RazorTemplate.Test.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + diff --git a/Template/Lab.RazorTemplate/Lab.RazorTemplate.Test/Usings.cs b/Template/Lab.RazorTemplate/Lab.RazorTemplate.Test/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/Template/Lab.RazorTemplate/Lab.RazorTemplate.Test/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/Template/Lab.RazorTemplate/Lab.RazorTemplate.sln b/Template/Lab.RazorTemplate/Lab.RazorTemplate.sln new file mode 100644 index 00000000..cc567ad9 --- /dev/null +++ b/Template/Lab.RazorTemplate/Lab.RazorTemplate.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.RazorTemplate", "Lab.RazorTemplate\Lab.RazorTemplate.csproj", "{4AC18C9C-3D58-4CD8-99D2-EA5C1ED9B1EA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.RazorTemplate.Test", "Lab.RazorTemplate.Test\Lab.RazorTemplate.Test.csproj", "{9FA27B86-E221-4D17-866F-DE5B2ED983CA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4AC18C9C-3D58-4CD8-99D2-EA5C1ED9B1EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4AC18C9C-3D58-4CD8-99D2-EA5C1ED9B1EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4AC18C9C-3D58-4CD8-99D2-EA5C1ED9B1EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4AC18C9C-3D58-4CD8-99D2-EA5C1ED9B1EA}.Release|Any CPU.Build.0 = Release|Any CPU + {9FA27B86-E221-4D17-866F-DE5B2ED983CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FA27B86-E221-4D17-866F-DE5B2ED983CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FA27B86-E221-4D17-866F-DE5B2ED983CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FA27B86-E221-4D17-866F-DE5B2ED983CA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Template/Lab.RazorTemplate/Lab.RazorTemplate/EnvTemplate.cshtml b/Template/Lab.RazorTemplate/Lab.RazorTemplate/EnvTemplate.cshtml new file mode 100644 index 00000000..e039e1a2 --- /dev/null +++ b/Template/Lab.RazorTemplate/Lab.RazorTemplate/EnvTemplate.cshtml @@ -0,0 +1,32 @@ +@using Lab.RazorTemplate +@model Lab.RazorTemplate.K8sValue +@{ + var _market = ViewBag.Market; + var _environment = ViewBag.Environment; + Console.WriteLine($"{nameof(_market)} = {_market}"); + Console.WriteLine($"{nameof(_environment)} = {_environment}"); +} +@{ + var K8S_COMMON_SERVICE_NAME = new Dictionary() + { + { MarketName.TW, "k8s-common-tw" }, + { MarketName.HK, "k8s-common-hk" }, + { MarketName.MY, "k8s-common-my" }, + }; + Console.WriteLine($"{nameof(K8S_COMMON_SERVICE_NAME)} = {K8S_COMMON_SERVICE_NAME}"); +} +K8S_COMMON_SERVICE_NAME= @K8S_COMMON_SERVICE_NAME[_market] +@{ + string NMQ_APIMIN_BASE_URL = null; + if (_environment == EnvironmentName.Dev) + { + NMQ_APIMIN_BASE_URL = "ABC"; + } + else if (_environment == EnvironmentName.QA + && _market == MarketName.TW) + { + NMQ_APIMIN_BASE_URL = "DEF"; + } + Console.WriteLine($"{nameof(NMQ_APIMIN_BASE_URL)} = {NMQ_APIMIN_BASE_URL}"); +} +NMQ_APIMIN_BASE_URL = @NMQ_APIMIN_BASE_URL; \ No newline at end of file diff --git a/Template/Lab.RazorTemplate/Lab.RazorTemplate/EnvironmentType.cs b/Template/Lab.RazorTemplate/Lab.RazorTemplate/EnvironmentType.cs new file mode 100644 index 00000000..0cf65b4a --- /dev/null +++ b/Template/Lab.RazorTemplate/Lab.RazorTemplate/EnvironmentType.cs @@ -0,0 +1,9 @@ +namespace Lab.RazorTemplate; + +public class EnvironmentName +{ + public const string Dev = "Dev"; + public const string Test = "Test"; + public const string QA = "QA"; + public const string Production = "Prod"; +} \ No newline at end of file diff --git a/Template/Lab.RazorTemplate/Lab.RazorTemplate/K8sTemplateEngine.cs b/Template/Lab.RazorTemplate/Lab.RazorTemplate/K8sTemplateEngine.cs new file mode 100644 index 00000000..14e417b1 --- /dev/null +++ b/Template/Lab.RazorTemplate/Lab.RazorTemplate/K8sTemplateEngine.cs @@ -0,0 +1,15 @@ +using System.Text; + +namespace Lab.RazorTemplate; + +public class K8sTemplateEngine +{ + public async Task RenderAsync(string templatePath, + K8sValue k8sValue, + Dictionary k8sDynamicValue) + { + return await Razor.Templating.Core.RazorTemplateEngine.RenderAsync(templatePath, + k8sValue, + k8sDynamicValue); + } +} diff --git a/Template/Lab.RazorTemplate/Lab.RazorTemplate/K8sValue.cs b/Template/Lab.RazorTemplate/Lab.RazorTemplate/K8sValue.cs new file mode 100644 index 00000000..795e6fd8 --- /dev/null +++ b/Template/Lab.RazorTemplate/Lab.RazorTemplate/K8sValue.cs @@ -0,0 +1,22 @@ +namespace Lab.RazorTemplate; + +public class K8sValue +{ + public Common Common { get; set; } + + public Resource Resource { get; set; } +} + +public class Common +{ + public string ProjectName { get; set; } + + public string Namespace { get; set; } +} + +public class Resource +{ + public uint CPU { get; set; } + + public uint Memory { get; set; } +} diff --git a/Template/Lab.RazorTemplate/Lab.RazorTemplate/Lab.RazorTemplate.csproj b/Template/Lab.RazorTemplate/Lab.RazorTemplate/Lab.RazorTemplate.csproj new file mode 100644 index 00000000..41c69183 --- /dev/null +++ b/Template/Lab.RazorTemplate/Lab.RazorTemplate/Lab.RazorTemplate.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + true + + + + + + + + diff --git a/Template/Lab.RazorTemplate/Lab.RazorTemplate/MarketType.cs b/Template/Lab.RazorTemplate/Lab.RazorTemplate/MarketType.cs new file mode 100644 index 00000000..5948fd30 --- /dev/null +++ b/Template/Lab.RazorTemplate/Lab.RazorTemplate/MarketType.cs @@ -0,0 +1,8 @@ +namespace Lab.RazorTemplate; + +public class MarketName +{ + public const string TW = "TW"; + public const string MY = "MY"; + public const string HK = "HK"; +} \ No newline at end of file diff --git a/Template/Lab.RazorTemplate/Lab.RazorTemplate/Template.ConfigMap.cshtml b/Template/Lab.RazorTemplate/Lab.RazorTemplate/Template.ConfigMap.cshtml new file mode 100644 index 00000000..348b1de3 --- /dev/null +++ b/Template/Lab.RazorTemplate/Lab.RazorTemplate/Template.ConfigMap.cshtml @@ -0,0 +1,9 @@ +@model Lab.RazorTemplate.K8sValue +apiVersion: v1 +kind: ConfigMap +metadata: +name: @Model.Common.ProjectName +namespace: @Model.Common.Namespace +spec1: @ViewData["Value1"] +spec2: @ViewBag.Value2 +spec3: @ViewBag.K8S_COMMON_SERVICE_NAME \ No newline at end of file diff --git a/Test/Lab.AllureReport/.gitignore b/Test/Lab.AllureReport/.gitignore new file mode 100644 index 00000000..a33719ce --- /dev/null +++ b/Test/Lab.AllureReport/.gitignore @@ -0,0 +1,365 @@ +### VisualStudio template +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +*.feature.cs diff --git a/Test/Lab.AllureReport/Lab.AllureReport.sln b/Test/Lab.AllureReport/Lab.AllureReport.sln new file mode 100644 index 00000000..625a34bd --- /dev/null +++ b/Test/Lab.AllureReport/Lab.AllureReport.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.AllureReport4Specflow", "Lab.AllureReport4Specflow\Lab.AllureReport4Specflow.csproj", "{24AC2522-D7B4-4854-BE1E-18389125605F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {24AC2522-D7B4-4854-BE1E-18389125605F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24AC2522-D7B4-4854-BE1E-18389125605F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24AC2522-D7B4-4854-BE1E-18389125605F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24AC2522-D7B4-4854-BE1E-18389125605F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Test/Lab.AllureReport/Lab.AllureReport4Specflow/.runsettings b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/.runsettings new file mode 100644 index 00000000..2f5e2ef5 --- /dev/null +++ b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/.runsettings @@ -0,0 +1,9 @@ + + + + .\TestResults + + trx + + + diff --git a/Test/Lab.AllureReport/Lab.AllureReport4Specflow/Calculation.cs b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/Calculation.cs new file mode 100644 index 00000000..4880a559 --- /dev/null +++ b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/Calculation.cs @@ -0,0 +1,9 @@ +namespace Lab.AllureReport4Specflow; + +public class Calculation +{ + public double Add(double firstNumber, double secondNumber) + { + return firstNumber + secondNumber; + } +} \ No newline at end of file diff --git a/Test/Lab.AllureReport/Lab.AllureReport4Specflow/Lab.AllureReport4Specflow.csproj b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/Lab.AllureReport4Specflow.csproj new file mode 100644 index 00000000..585d3b0a --- /dev/null +++ b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/Lab.AllureReport4Specflow.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + diff --git a/Test/Lab.AllureReport/Lab.AllureReport4Specflow/Tests.cs b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/Tests.cs new file mode 100644 index 00000000..5bfbd532 --- /dev/null +++ b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/Tests.cs @@ -0,0 +1,26 @@ +using Allure.Commons; +using NUnit.Allure.Attributes; +using NUnit.Allure.Core; +using NUnit.Framework; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; + +namespace Lab.AllureReport4Specflow; + +[TestFixture] +[AllureNUnit] +[AllureSubSuite("Example")] +[AllureSeverity(SeverityLevel.critical)] +public class Tests +{ + [Test] + [AllureTag("NUnit","Debug")] + [AllureIssue("GitHub#1", "https://github.com/unickq/allure-nunit")] + [AllureFeature("Core")] + [TestCase(20, 50, 70)] + public void 相加兩個數字(double firstNumber, double secondNumber, double expected) + { + var calculation = new Calculation(); + var actual = calculation.Add(firstNumber, secondNumber); + Assert.AreEqual(expected, actual); + } +} \ No newline at end of file diff --git a/Test/Lab.AllureReport/Lab.AllureReport4Specflow/UnitTests.cs b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/UnitTests.cs new file mode 100644 index 00000000..7e06cfee --- /dev/null +++ b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/UnitTests.cs @@ -0,0 +1,16 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.AllureReport4Specflow; + +[TestClass] +public class UnitTests +{ + [TestMethod] + [DataRow(50,70,120)] + public void 相加兩個數字(double firstNumber, double secondNumber, double expected) + { + var calculation = new Calculation(); + var actual = calculation.Add(firstNumber, secondNumber); + Assert.AreEqual(expected, actual); + } +} \ No newline at end of file diff --git a/Test/Lab.AllureReport/Lab.AllureReport4Specflow/specflow.json b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/specflow.json new file mode 100644 index 00000000..06565c4c --- /dev/null +++ b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/specflow.json @@ -0,0 +1,10 @@ +{ + "language": { + "feature": "en-US" + }, + "stepAssemblies": [ + { + "assembly": "Allure.SpecFlowPlugin" + } + ] +} \ No newline at end of file diff --git "a/Test/Lab.AllureReport/Lab.AllureReport4Specflow/\350\250\210\347\256\227\346\251\237.feature" "b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/\350\250\210\347\256\227\346\251\237.feature" new file mode 100644 index 00000000..4c4f7712 --- /dev/null +++ "b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/\350\250\210\347\256\227\346\251\237.feature" @@ -0,0 +1,21 @@ +Feature: 計算機 +Simple calculator for adding two numbers + + @mytag + Scenario: 相加兩個數字 + Given 第一個數字為 50 + And 第二個數字為 70 + When 兩個數字相加 + Then 結果應該為 120 + + Scenario Outline: 相加兩個數字(Examples) + Given 第一個數字為 + And 第二個數字為 + When 兩個數字相加 + Then 結果應該為 + + Examples: + | First | Second | Result | + | 50 | 70 | 120 | + | 30 | 40 | 70 | + | 60 | 30 | 90 | \ No newline at end of file diff --git "a/Test/Lab.AllureReport/Lab.AllureReport4Specflow/\350\250\210\347\256\227\346\251\237Step.cs" "b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/\350\250\210\347\256\227\346\251\237Step.cs" new file mode 100644 index 00000000..10ba428f --- /dev/null +++ "b/Test/Lab.AllureReport/Lab.AllureReport4Specflow/\350\250\210\347\256\227\346\251\237Step.cs" @@ -0,0 +1,37 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TechTalk.SpecFlow; + +namespace Lab.AllureReport4Specflow; + +[Binding] +public class 計算機Step : Steps +{ + [Given(@"第一個數字為 (.*)")] + public void Given第一個數字為(double firstNumber) + { + this.ScenarioContext.Set(firstNumber, "firstNumber"); + } + + [Given(@"第二個數字為 (.*)")] + public void Given第二個數字為(double secondNumber) + { + this.ScenarioContext.Set(secondNumber, "secondNumber"); + } + + [Then(@"結果應該為 (.*)")] + public void Then結果應該為(double expected) + { + var actual = this.ScenarioContext.Get("actual"); + Assert.AreEqual(expected, actual); + } + + [When(@"兩個數字相加")] + public void When兩個數字相加() + { + var firstNumber = this.ScenarioContext.Get("firstNumber"); + var secondNumber = this.ScenarioContext.Get("secondNumber"); + var calculation = new Calculation(); + var actual = calculation.Add(firstNumber, secondNumber); + this.ScenarioContext.Set(actual, "actual"); + } +} \ No newline at end of file diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/Lab.AspNetCoreMiddleware.UnitTest.csproj b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/Lab.AspNetCoreMiddleware.UnitTest.csproj new file mode 100644 index 00000000..37e06a28 --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/Lab.AspNetCoreMiddleware.UnitTest.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + + diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/ValidateRequireHeaderMiddlewareTests.cs b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/ValidateRequireHeaderMiddlewareTests.cs new file mode 100644 index 00000000..fd44b1bd --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/ValidateRequireHeaderMiddlewareTests.cs @@ -0,0 +1,106 @@ +using System; +using System.IO; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.JsonDiffPatch.MsTest; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.AspNetCoreMiddleware.UnitTest; + +[TestClass] +public class ValidateRequireHeaderMiddlewareTests +{ + [TestMethod] + public async Task HeaderCode型別錯誤會驗證失敗() + { + var expected = @" +{ + ""code"": ""INVALID_REQUEST"", + ""messages"": [ + { + ""code"": ""INVALID_TYPE"", + ""propertyName"": ""X-Code"", + ""messages"": ""'abc' not numbers"", + ""value"": ""abc"" + } + ] +} +"; + using var testServer = await CreateTestServer(); + var httpContext = await testServer.SendAsync(context => + { + context.Request.Headers[HeaderNames.UserId] = "yao"; + context.Request.Headers[HeaderNames.Code] = "abc"; + }); + var response = httpContext.Response; + var stream = response.Body; + + var actual = await new StreamReader(stream).ReadToEndAsync(); + Assert.That.JsonAreEqual(expected, actual, true); + } + + [TestMethod] + public async Task 所有Header為空會驗證失敗() + { + var expected = @" +{ + ""code"": ""INVALID_REQUEST"", + ""messages"": [ + { + ""code"": ""INVALID_FORMAT"", + ""propertyName"": ""X-User-Id"", + ""messages"": ""The 'X-User-Id' header is required."" + }, + { + ""code"": ""INVALID_FORMAT"", + ""propertyName"": ""X-Code"", + ""messages"": ""The 'X-Code' header is required."" + } + ] +} +"; + using var testServer = await CreateTestServer(); + var httpContext = await testServer.SendAsync(context => { }); + var response = httpContext.Response; + var stream = response.Body; + + var actual = await new StreamReader(stream).ReadToEndAsync(); + Assert.That.JsonAreEqual(expected, actual, true); + } + + private static async Task CreateTestServer() + { + var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseTestServer() + .ConfigureServices( + services => + { + services.AddSingleton(p => new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false, + + // Encoder = JavaScriptEncoder.Create(UnicodeRanges.All, UnicodeRanges.All), + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }); + }) + .Configure(app => { app.UseMiddleware(); }); + }) + .StartAsync(); + + var server = host.GetTestServer(); + server.BaseAddress = new Uri("https://我真的是假的/不要打我的臉/"); + return server; + } +} \ No newline at end of file diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/ValidateRequireHeaderMiddlewareTests1.cs b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/ValidateRequireHeaderMiddlewareTests1.cs new file mode 100644 index 00000000..61530a38 --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/ValidateRequireHeaderMiddlewareTests1.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.JsonDiffPatch.MsTest; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.AspNetCoreMiddleware.UnitTest; + +[TestClass] +public class ValidateRequireHeaderMiddlewareTests1 +{ + [TestMethod] + public async Task HeaderCode型別錯誤會驗證失敗() + { + var expected = @" +{ + ""code"": ""INVALID_REQUEST"", + ""messages"": [ + { + ""code"": ""INVALID_TYPE"", + ""propertyName"": ""X-Code"", + ""messages"": ""'abc' not numbers"", + ""value"": ""abc"" + } + ] +} +"; + using var httpClient = await CreateTestClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "/青菜"); + request.Headers.Add(HeaderNames.UserId, "yao"); + request.Headers.Add(HeaderNames.Code, "abc"); + var response = await httpClient.SendAsync(request); + var actual = await response.Content.ReadAsStringAsync(); + Assert.That.JsonAreEqual(expected, actual, true); + } + + [TestMethod] + public async Task 所有Header為空會驗證失敗() + { + var expected = @" +{ + ""code"": ""INVALID_REQUEST"", + ""messages"": [ + { + ""code"": ""INVALID_FORMAT"", + ""propertyName"": ""X-User-Id"", + ""messages"": ""The 'X-User-Id' header is required."" + }, + { + ""code"": ""INVALID_FORMAT"", + ""propertyName"": ""X-Code"", + ""messages"": ""The 'X-Code' header is required."" + } + ] +} +"; + using var httpClient = await CreateTestClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "/青菜"); + var response = await httpClient.SendAsync(request); + var actual = await response.Content.ReadAsStringAsync(); + Assert.That.JsonAreEqual(expected, actual, true); + } + + private static async Task CreateTestClient() + { + var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseTestServer() + .ConfigureServices( + services => + { + services.AddSingleton(p => new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false, + + // Encoder = JavaScriptEncoder.Create(UnicodeRanges.All, UnicodeRanges.All), + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }); + }) + .Configure(app => { app.UseMiddleware(); }); + }) + .StartAsync(); + return host.GetTestClient(); + } +} \ No newline at end of file diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/ValidateRequireHeaderMiddlewareTests2.cs b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/ValidateRequireHeaderMiddlewareTests2.cs new file mode 100644 index 00000000..aa36cda6 --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.UnitTest/ValidateRequireHeaderMiddlewareTests2.cs @@ -0,0 +1,109 @@ +using System; +using System.IO; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.JsonDiffPatch.MsTest; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.AspNetCoreMiddleware.UnitTest; + +[TestClass] +public class ValidateRequireHeaderMiddlewareTests2 +{ + [TestMethod] + public async Task HeaderCode型別錯誤會驗證失敗() + { + var expected = @" +{ + ""code"": ""INVALID_REQUEST"", + ""messages"": [ + { + ""code"": ""INVALID_TYPE"", + ""propertyName"": ""X-Code"", + ""messages"": ""'abc' not numbers"", + ""value"": ""abc"" + } + ] +} +"; + var jsonSerializerOptions = CreateJsonSerializerOptions(); + var httpContext = new DefaultHttpContext() + { + Response = { Body = new MemoryStream()} + }; + httpContext.Request.Headers[HeaderNames.UserId] = "yao"; + httpContext.Request.Headers[HeaderNames.Code] = "abc"; + var target = new ValidateRequiredHeaderMiddleware((_) => Task.CompletedTask); + await target.InvokeAsync(httpContext, jsonSerializerOptions); + var response = httpContext.Response; + var stream = response.Body; + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + var actual = await new StreamReader(stream).ReadToEndAsync(); + Assert.That.JsonAreEqual(expected, actual, true); + } + + [TestMethod] + public async Task 所有Header為空會驗證失敗() + { + var expected = @" +{ + ""code"": ""INVALID_REQUEST"", + ""messages"": [ + { + ""code"": ""INVALID_FORMAT"", + ""propertyName"": ""X-User-Id"", + ""messages"": ""The 'X-User-Id' header is required."" + }, + { + ""code"": ""INVALID_FORMAT"", + ""propertyName"": ""X-Code"", + ""messages"": ""The 'X-Code' header is required."" + } + ] +} +"; + var jsonSerializerOptions = CreateJsonSerializerOptions(); + var httpContext = new DefaultHttpContext + { + Response = { Body = new MemoryStream() } + }; + + var target = new ValidateRequiredHeaderMiddleware(_ => Task.CompletedTask); + await target.InvokeAsync(httpContext, jsonSerializerOptions); + var response = httpContext.Response; + var stream = response.Body; + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + var actual = await new StreamReader(stream).ReadToEndAsync(); + Assert.That.JsonAreEqual(expected, actual, true); + } + + private static JsonSerializerOptions CreateJsonSerializerOptions() + { + return new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false, + + // Encoder = JavaScriptEncoder.Create(UnicodeRanges.All, UnicodeRanges.All), + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + } +} \ No newline at end of file diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/Lab.AspNetCoreMiddleware.Web.csproj b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/Lab.AspNetCoreMiddleware.Web.csproj new file mode 100644 index 00000000..d52487bf --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/Lab.AspNetCoreMiddleware.Web.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/Program.cs b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/Program.cs new file mode 100644 index 00000000..d61665b8 --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/Program.cs @@ -0,0 +1,7 @@ +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +app.MapGet("/", () => "Hello World!"); + +app.Run(); +public partial class Program { } \ No newline at end of file diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/Properties/launchSettings.json b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/Properties/launchSettings.json new file mode 100644 index 00000000..e1658380 --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55168", + "sslPort": 44383 + } + }, + "profiles": { + "Lab.AspNetCoreMiddleware.Web": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7251;http://localhost:5251", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/appsettings.Development.json b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/appsettings.json b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.Web/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.sln b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.sln new file mode 100644 index 00000000..e2a0a451 --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.AspNetCoreMiddleware", "Lab.AspNetCoreMiddleware\Lab.AspNetCoreMiddleware.csproj", "{FA33EE2C-5954-457C-9A00-ED3FDD38010C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.AspNetCoreMiddleware.UnitTest", "Lab.AspNetCoreMiddleware.UnitTest\Lab.AspNetCoreMiddleware.UnitTest.csproj", "{FAB4FE0D-4232-46EA-A0A4-038D419E208F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.AspNetCoreMiddleware.Web", "Lab.AspNetCoreMiddleware.Web\Lab.AspNetCoreMiddleware.Web.csproj", "{93EB2B91-FE4B-4A90-B517-8A959AD0BC40}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FA33EE2C-5954-457C-9A00-ED3FDD38010C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA33EE2C-5954-457C-9A00-ED3FDD38010C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA33EE2C-5954-457C-9A00-ED3FDD38010C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA33EE2C-5954-457C-9A00-ED3FDD38010C}.Release|Any CPU.Build.0 = Release|Any CPU + {FAB4FE0D-4232-46EA-A0A4-038D419E208F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAB4FE0D-4232-46EA-A0A4-038D419E208F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAB4FE0D-4232-46EA-A0A4-038D419E208F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAB4FE0D-4232-46EA-A0A4-038D419E208F}.Release|Any CPU.Build.0 = Release|Any CPU + {93EB2B91-FE4B-4A90-B517-8A959AD0BC40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93EB2B91-FE4B-4A90-B517-8A959AD0BC40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93EB2B91-FE4B-4A90-B517-8A959AD0BC40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93EB2B91-FE4B-4A90-B517-8A959AD0BC40}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/Failure.cs b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/Failure.cs new file mode 100644 index 00000000..24909aa8 --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/Failure.cs @@ -0,0 +1,8 @@ +namespace Lab.AspNetCoreMiddleware; + +internal class Failure +{ + public string Code { get; init; } + + public IEnumerable Messages { get; init; } +} \ No newline at end of file diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/FailureCode.cs b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/FailureCode.cs new file mode 100644 index 00000000..aa0463a7 --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/FailureCode.cs @@ -0,0 +1,8 @@ +namespace Lab.AspNetCoreMiddleware; + +enum FailureCode +{ + INVALID_FORMAT, + INVALID_REQUEST, + INVALID_TYPE +} \ No newline at end of file diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/FailureResult.cs b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/FailureResult.cs new file mode 100644 index 00000000..6c659c4d --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/FailureResult.cs @@ -0,0 +1,12 @@ +namespace Lab.AspNetCoreMiddleware; + +internal class FailureResult +{ + public string Code { get; init; } + + public string PropertyName { get; init; } + + public string Messages { get; init; } + + public string Value { get; init ; } +} \ No newline at end of file diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/HeaderNames.cs b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/HeaderNames.cs new file mode 100644 index 00000000..b3a8dd66 --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/HeaderNames.cs @@ -0,0 +1,7 @@ +namespace Lab.AspNetCoreMiddleware; + +public class HeaderNames +{ + public static string UserId = "X-User-Id"; + public static string Code = "X-Code"; +} \ No newline at end of file diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.csproj b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.csproj new file mode 100644 index 00000000..2a1c792b --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/ValidateRequiredHeaderMiddleware.cs b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/ValidateRequiredHeaderMiddleware.cs new file mode 100644 index 00000000..7b8b594b --- /dev/null +++ b/Test/Lab.AspNetCoreMiddleware/Lab.AspNetCoreMiddleware/ValidateRequiredHeaderMiddleware.cs @@ -0,0 +1,71 @@ +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; + +namespace Lab.AspNetCoreMiddleware; + +public class ValidateRequiredHeaderMiddleware +{ + private readonly string[] _requireHeaderNames = + { + HeaderNames.UserId, + HeaderNames.Code, + }; + + private readonly RequestDelegate _next; + + public ValidateRequiredHeaderMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context, + JsonSerializerOptions jsonSerializerOptions) + { + var failureResults = new List(); + foreach (var name in this._requireHeaderNames) + { + if (context.Request.Headers.TryGetValue(name, out var value) == false) + { + failureResults.Add(new FailureResult + { + Code = FailureCode.INVALID_FORMAT.ToString(), + PropertyName = name, + Messages = $"The '{name}' header is required.", + }); + } + else + { + if (name == HeaderNames.Code) + { + if (long.TryParse(value, out var code) == false) + { + failureResults.Add(new FailureResult + { + Code = FailureCode.INVALID_TYPE.ToString(), + PropertyName = name, + Value = value, + Messages = $"'{value}' not numbers", + }); + } + } + } + } + + if (failureResults.Count > 0) + { + var failure = new Failure + { + Code = FailureCode.INVALID_REQUEST.ToString(), + Messages = failureResults + }; + var failureJson = JsonSerializer.Serialize(failure, jsonSerializerOptions); + context.Response.StatusCode = 400; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(failureJson, Encoding.UTF8, context.RequestAborted); + return; + } + + await this._next(context); + } +} \ No newline at end of file diff --git a/Test/Lab.BDD.Pipe/Lab.BDD.Pipe.TestProject/Lab.BDD.Pipe.TestProject.csproj b/Test/Lab.BDD.Pipe/Lab.BDD.Pipe.TestProject/Lab.BDD.Pipe.TestProject.csproj new file mode 100644 index 00000000..1fdc25af --- /dev/null +++ b/Test/Lab.BDD.Pipe/Lab.BDD.Pipe.TestProject/Lab.BDD.Pipe.TestProject.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + diff --git a/Test/Lab.BDD.Pipe/Lab.BDD.Pipe.TestProject/UnitTest1.cs b/Test/Lab.BDD.Pipe/Lab.BDD.Pipe.TestProject/UnitTest1.cs new file mode 100644 index 00000000..ab3f3637 --- /dev/null +++ b/Test/Lab.BDD.Pipe/Lab.BDD.Pipe.TestProject/UnitTest1.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using BddPipe; +using BddPipe.Model; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using static BddPipe.Runner; +using BddPipe.Recipe; + +namespace Lab.BDD.Pipe.TestProject; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void 相加兩個數字() + { + Scenario() + .Given("有兩個數字", () => new { firstNumber = (decimal)5, secondNumber = (decimal)10 }) + .When("按下相加", setup => + { + var calculation = new Calculation(); + return calculation.Add(setup.firstNumber, setup.secondNumber); + }) + .Then("預期得到", actual => + { + var expected = 15; + Assert.AreEqual(expected, actual); + }) + .Run(); + } +} + +public class Calculation +{ + public decimal Add(decimal firstNumber, decimal secondNumber) + { + return firstNumber + secondNumber; + } +} \ No newline at end of file diff --git a/Test/Lab.BDD.Pipe/Lab.BDD.Pipe.sln b/Test/Lab.BDD.Pipe/Lab.BDD.Pipe.sln new file mode 100644 index 00000000..4b518bf3 --- /dev/null +++ b/Test/Lab.BDD.Pipe/Lab.BDD.Pipe.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.BDD.Pipe.TestProject", "Lab.BDD.Pipe.TestProject\Lab.BDD.Pipe.TestProject.csproj", "{737A0583-DB1E-4C63-8552-EE6ED19700A8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {737A0583-DB1E-4C63-8552-EE6ED19700A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {737A0583-DB1E-4C63-8552-EE6ED19700A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {737A0583-DB1E-4C63-8552-EE6ED19700A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {737A0583-DB1E-4C63-8552-EE6ED19700A8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Test/MultiTestCase/Makefile b/Test/MultiTestCase/Makefile new file mode 100644 index 00000000..cf17f07f --- /dev/null +++ b/Test/MultiTestCase/Makefile @@ -0,0 +1,4 @@ +G1: + echo 'Hello World' +G2: + cmd \ No newline at end of file diff --git a/Test/MultiTestCase/MultiTestCase.sln b/Test/MultiTestCase/MultiTestCase.sln new file mode 100644 index 00000000..fe689a03 --- /dev/null +++ b/Test/MultiTestCase/MultiTestCase.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{1F776B7D-C182-4DC3-BA57-844657BD9AC0}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + Makefile = Makefile + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{483A9EB7-C4AF-4D68-80ED-49277985C760}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.MultiTestCase", "src\Lab.MultiTestCase\Lab.MultiTestCase.csproj", "{8F6CEAB7-4486-4AD6-8AAE-5DBEDA449E57}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.MultiTestCase.UnitTest", "src\Lab.MultiTestCase.UnitTest\Lab.MultiTestCase.UnitTest.csproj", "{8F1C53C4-CE7C-447A-B627-3D428DA818A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Domain", "src\Lab.Domain\Lab.Domain.csproj", "{A3EFE099-E9FB-41F4-9718-087AFA4E0A7B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Infrastructure.DB", "src\Lab.Infrastructure.DB\Lab.Infrastructure.DB.csproj", "{CD1E06DF-79FC-4189-88D8-05F4E5C8DC7B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8F6CEAB7-4486-4AD6-8AAE-5DBEDA449E57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F6CEAB7-4486-4AD6-8AAE-5DBEDA449E57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F6CEAB7-4486-4AD6-8AAE-5DBEDA449E57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F6CEAB7-4486-4AD6-8AAE-5DBEDA449E57}.Release|Any CPU.Build.0 = Release|Any CPU + {8F1C53C4-CE7C-447A-B627-3D428DA818A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F1C53C4-CE7C-447A-B627-3D428DA818A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F1C53C4-CE7C-447A-B627-3D428DA818A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F1C53C4-CE7C-447A-B627-3D428DA818A2}.Release|Any CPU.Build.0 = Release|Any CPU + {A3EFE099-E9FB-41F4-9718-087AFA4E0A7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3EFE099-E9FB-41F4-9718-087AFA4E0A7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3EFE099-E9FB-41F4-9718-087AFA4E0A7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3EFE099-E9FB-41F4-9718-087AFA4E0A7B}.Release|Any CPU.Build.0 = Release|Any CPU + {CD1E06DF-79FC-4189-88D8-05F4E5C8DC7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD1E06DF-79FC-4189-88D8-05F4E5C8DC7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD1E06DF-79FC-4189-88D8-05F4E5C8DC7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD1E06DF-79FC-4189-88D8-05F4E5C8DC7B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8F6CEAB7-4486-4AD6-8AAE-5DBEDA449E57} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {8F1C53C4-CE7C-447A-B627-3D428DA818A2} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {A3EFE099-E9FB-41F4-9718-087AFA4E0A7B} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + {CD1E06DF-79FC-4189-88D8-05F4E5C8DC7B} = {483A9EB7-C4AF-4D68-80ED-49277985C760} + EndGlobalSection +EndGlobal diff --git a/Test/MultiTestCase/docker-compose.yml b/Test/MultiTestCase/docker-compose.yml new file mode 100644 index 00000000..8ecb1567 --- /dev/null +++ b/Test/MultiTestCase/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.8" + +services: + db-sql: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=pass@w0rd1~ + ports: + - 1433:1433 \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.Domain/EmployeeAggregate/Entity/Employee.cs b/Test/MultiTestCase/src/Lab.Domain/EmployeeAggregate/Entity/Employee.cs new file mode 100644 index 00000000..85d92ad1 --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Domain/EmployeeAggregate/Entity/Employee.cs @@ -0,0 +1,29 @@ +namespace Lab.Domain.Entity; + +public record Employee +{ + public Guid Id { get; set; } + + public string Name { get; set; } + + public int? Age { get; set; } + + public string Remark { get; set; } + + public DateTimeOffset CreateAt { get; set; } + + public string CreateBy { get; set; } + + public Employee SetName(string name) + { + this.Name = name; + return this; + } + + public Employee SetAge(int age) + { + this.Age = age; + return this; + } + +} \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.Domain/EmployeeAggregate/IEmployeeAggregate.cs b/Test/MultiTestCase/src/Lab.Domain/EmployeeAggregate/IEmployeeAggregate.cs new file mode 100644 index 00000000..4d72246e --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Domain/EmployeeAggregate/IEmployeeAggregate.cs @@ -0,0 +1,8 @@ +using Lab.Domain.Entity; + +namespace Lab.Domain; + +public interface IEmployeeAggregate +{ + Employee InsertAsync(Employee employee, CancellationToken cancel = default); +} \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs b/Test/MultiTestCase/src/Lab.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs new file mode 100644 index 00000000..898a9e1d --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Domain/EmployeeAggregate/Repository/IEmployeeRepository.cs @@ -0,0 +1,16 @@ +using Lab.Domain.Entity; + +namespace Lab.Domain.Repository; + +public interface IEmployeeRepository +{ + Task InsertAsync(Employee employee, CancellationToken cancel = default); +} + +class EmployeeRepository : IEmployeeRepository +{ + public Task InsertAsync(Employee employee, CancellationToken cancel = default) + { + throw new NotImplementedException(); + } +} diff --git a/Test/MultiTestCase/src/Lab.Domain/Lab.Domain.csproj b/Test/MultiTestCase/src/Lab.Domain/Lab.Domain.csproj new file mode 100644 index 00000000..eb2460e9 --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Domain/Lab.Domain.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Test/MultiTestCase/src/Lab.Infrastructure.DB/AppDependencyInjectionExtensions.cs b/Test/MultiTestCase/src/Lab.Infrastructure.DB/AppDependencyInjectionExtensions.cs new file mode 100644 index 00000000..c0960243 --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Infrastructure.DB/AppDependencyInjectionExtensions.cs @@ -0,0 +1,52 @@ +using Lab.Infrastructure.DB.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.Infrastructure.DB; + +public static class AppDependencyInjectionExtensions +{ + public static void AddAppEnvironment(this IServiceCollection services) + { + services.AddLogging(builder => + { + builder.AddConsole(); + }); + services.AddSingleton(); + } + + public static void AddEntityFramework(this IServiceCollection services) + { + services.AddPooledDbContextFactory((provider, optionsBuilder) => + { + var option = provider.GetService(); + var connectionString = option.EmployeeDbConnectionString; + var loggerFactory = provider.GetService(); + optionsBuilder.UseSqlServer(connectionString) + .UseLoggerFactory(loggerFactory) + ; + }); + + ; + + // services.AddPooledDbContextFactory((provider, options) => + // { + // var option = provider.GetService(); + // var loggerFactory = provider.GetService(); + // options.UseNpgsql( + // option.MemberDbConnectionString, //只會呼叫一次 + // builder => + // builder.EnableRetryOnFailure( + // 10, + // TimeSpan.FromSeconds(30), + // new List { "57P01" })) + // + // // .UseLazyLoadingProxies() + // .UseSnakeCaseNamingConvention() + // .UseLoggerFactory(loggerFactory) + // ; + // }); + } + +} \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.Infrastructure.DB/AppEnvironmentOption.cs b/Test/MultiTestCase/src/Lab.Infrastructure.DB/AppEnvironmentOption.cs new file mode 100644 index 00000000..eb657155 --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Infrastructure.DB/AppEnvironmentOption.cs @@ -0,0 +1,31 @@ +namespace Lab.Infrastructure.DB; + +public class AppEnvironmentOption +{ + public string EmployeeDbConnectionString + { + get + { + if (string.IsNullOrWhiteSpace(this._employeeDbConnectionString)) + { + this._employeeDbConnectionString = + EnvironmentAssistant.GetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR); + } + + return this._employeeDbConnectionString; + } + set + { + this._employeeDbConnectionString = value; + Environment.SetEnvironmentVariable(this.EMPLOYEE_DB_CONN_STR, value); + } + } + + private string _employeeDbConnectionString; + private readonly string EMPLOYEE_DB_CONN_STR = "EMPLOYEE_DB_CONNECTION_STR"; + + public void Initial() + { + var memberDbConnectionString = this.EmployeeDbConnectionString; + } +} \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/Employee.cs b/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/Employee.cs new file mode 100644 index 00000000..f90ea3ad --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/Employee.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.Infrastructure.DB.EntityModel +{ + [Table("Employee")] + public class Employee + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Id { get; set; } + + [Required] + public string Name { get; set; } + + public int? Age { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + public string Remark { get; set; } + + [Required] + public DateTimeOffset CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + public virtual Identity Identity { get; set; } + } +} \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/EmployeeDbContext.cs b/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..e657ab04 --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.InMemory.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; + +namespace Lab.Infrastructure.DB.EntityModel +{ + public class EmployeeDbContext : DbContext + { + private static readonly bool[] s_migrated = { false }; + + public virtual DbSet Employees { get; set; } + + public virtual DbSet Identities { get; set; } + + public virtual DbSet OrderHistories { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + if (s_migrated[0]) + { + return; + } + + lock (s_migrated) + { + if (s_migrated[0] == false) + { + var memoryOptions = options.FindExtension(); + + if (memoryOptions == null) + { + var sqlOptions = options.FindExtension(); + if (sqlOptions != null) + { + Console.WriteLine( + $"EmployeeDbContext of connection string be '{sqlOptions.ConnectionString}'"); + this.Database.Migrate(); + } + } + + s_migrated[0] = true; + } + } + } + + //管理索引 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(p => + { + p.HasKey(e => e.Id) + .IsClustered(false); + + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + + p.Property(p => p.Remark) + .IsRequired(false) + ; + }); + + modelBuilder.Entity(p => + { + p.HasKey(e => e.Employee_Id) + .IsClustered(false); + + p.HasIndex(e => e.SequenceId) + .IsUnique() + .IsClustered(); + }); + } + } +} \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/Identity.cs b/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/Identity.cs new file mode 100644 index 00000000..fe8b8e3b --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/Identity.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.Infrastructure.DB.EntityModel +{ + [Table("Identity")] + public class Identity + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Employee_Id { get; set; } + + [Required] + public string Account { get; set; } + + [Required] + public string Password { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Remark { get; set; } + + [Required] + public DateTimeOffset CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + + [ForeignKey("Employee_Id")] + public virtual Employee Employee { get; set; } + } +} \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/OrderHistory.cs b/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/OrderHistory.cs new file mode 100644 index 00000000..0eb33d75 --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Infrastructure.DB/EntityModel/OrderHistory.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.Infrastructure.DB.EntityModel +{ + [Table("OrderHistory")] + public class OrderHistory + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public Guid? Employee_Id { get; set; } + + public string Remark { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } + + public string Product_Id { get; set; } + + public string Product_Name { get; set; } + + [Required] + public DateTime CreateAt { get; set; } + + [Required] + public string CreateBy { get; set; } + } +} \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.Infrastructure.DB/EnvironmentAssistant.cs b/Test/MultiTestCase/src/Lab.Infrastructure.DB/EnvironmentAssistant.cs new file mode 100644 index 00000000..72a3c6ee --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Infrastructure.DB/EnvironmentAssistant.cs @@ -0,0 +1,15 @@ +namespace Lab.Infrastructure.DB; + +public class EnvironmentAssistant +{ + public static string GetEnvironmentVariable(string key) + { + var result = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrWhiteSpace(result)) + { + throw new Exception($"the key '{key}' not exist in environment variable"); + } + + return result; + } +} \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.Infrastructure.DB/Lab.Infrastructure.DB.csproj b/Test/MultiTestCase/src/Lab.Infrastructure.DB/Lab.Infrastructure.DB.csproj new file mode 100644 index 00000000..9fcdb0d8 --- /dev/null +++ b/Test/MultiTestCase/src/Lab.Infrastructure.DB/Lab.Infrastructure.DB.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + + + + + + + + + diff --git a/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/Lab.MultiTestCase.UnitTest.csproj b/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/Lab.MultiTestCase.UnitTest.csproj new file mode 100644 index 00000000..ecb158eb --- /dev/null +++ b/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/Lab.MultiTestCase.UnitTest.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + + + + + + + diff --git a/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/MsTestHook.cs b/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/MsTestHook.cs new file mode 100644 index 00000000..c48c99fe --- /dev/null +++ b/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/MsTestHook.cs @@ -0,0 +1,31 @@ +// using Microsoft.VisualStudio.TestTools.UnitTesting; +// +// namespace Lab.MultiTestCase.UnitTest; +// +// [TestClass] +// public class MsTestHook +// { +// [AssemblyCleanup] +// public static void Cleanup() +// { +// TestInstanceManager.SetTestEnvironmentVariable(); +// var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); +// if (db.Database.CanConnect()) +// { +// db.Database.EnsureDeleted(); +// } +// } +// +// [AssemblyInitialize] +// public static void Setup(TestContext context) +// { +// TestInstanceManager.SetTestEnvironmentVariable(); +// var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext(); +// if (db.Database.CanConnect()) +// { +// db.Database.EnsureDeleted(); +// } +// +// db.Database.EnsureCreated(); +// } +// } \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/TestInstanceManager.cs b/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/TestInstanceManager.cs new file mode 100644 index 00000000..16ca6112 --- /dev/null +++ b/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/TestInstanceManager.cs @@ -0,0 +1,34 @@ +// using System; +// using Lab.MultiTestCase.EntityModel; +// using Microsoft.EntityFrameworkCore; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace Lab.MultiTestCase.UnitTest; +// +// internal class TestInstanceManager +// { +// private static IServiceProvider _serviceProvider; +// +// public static IDbContextFactory EmployeeDbContextFactory => +// _serviceProvider.GetService>(); +// +// static TestInstanceManager() +// { +// var services = new ServiceCollection(); +// ConfigureTestServices(services); +// } +// +// public static void ConfigureTestServices(IServiceCollection services) +// { +// services.AddAppEnvironment(); +// services.AddEntityFramework(); +// _serviceProvider = services.BuildServiceProvider(); +// } +// +// public static void SetTestEnvironmentVariable() +// { +// var option = _serviceProvider.GetService(); +// option.EmployeeDbConnectionString = +// "Data Source=localhost;Initial Catalog=EmployeeDb;Integrated Security=false;User ID=sa;Password=pass@w0rd1~;MultipleActiveResultSets=True;TrustServerCertificate=True"; +// } +// } \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/UnitTest1.cs b/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/UnitTest1.cs new file mode 100644 index 00000000..79d1a00c --- /dev/null +++ b/Test/MultiTestCase/src/Lab.MultiTestCase.UnitTest/UnitTest1.cs @@ -0,0 +1,24 @@ +using System; +using Lab.Domain.Entity; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.MultiTestCase.UnitTest; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void AddRanges() + { + var source = new Employee() + { + Id = Guid.NewGuid(), + Age = 18, + Name = "yao" + }; + + source.Age = 20; + Assert.AreEqual(18,source.Age); + } + +} \ No newline at end of file diff --git a/Test/MultiTestCase/src/Lab.MultiTestCase/Lab.MultiTestCase.csproj b/Test/MultiTestCase/src/Lab.MultiTestCase/Lab.MultiTestCase.csproj new file mode 100644 index 00000000..4ed8d60e --- /dev/null +++ b/Test/MultiTestCase/src/Lab.MultiTestCase/Lab.MultiTestCase.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5.TestProject/CustomTestServer.cs b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5.TestProject/CustomTestServer.cs new file mode 100644 index 00000000..a420a623 --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5.TestProject/CustomTestServer.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; + +namespace Lab.Test.WebApi.Net5.TestProject +{ + public class CustomTestServer : WebApplicationFactory + { + private void ConfigureServices(IServiceCollection services) + { + services.AddScoped(p => + { + var fileProvider = Substitute.For(); + fileProvider.Name().Returns("Fake FileProfile"); + return fileProvider; + }); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(this.ConfigureServices); + } + } +} \ No newline at end of file diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5.TestProject/Lab.Test.WebApi.Net5.TestProject.csproj b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5.TestProject/Lab.Test.WebApi.Net5.TestProject.csproj new file mode 100644 index 00000000..53390344 --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5.TestProject/Lab.Test.WebApi.Net5.TestProject.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + + + diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5.TestProject/SurveyWebApplicationFactory.cs b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5.TestProject/SurveyWebApplicationFactory.cs new file mode 100644 index 00000000..05c45143 --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5.TestProject/SurveyWebApplicationFactory.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.Test.WebApi.Net5.TestProject +{ + [TestClass] + public class SurveyWebApplicationFactory + { + [TestMethod] + public void CustomTestServer() + { + var server = new CustomTestServer(); + var httpClient = server.CreateClient(); + var url = "demo"; + var response = httpClient.GetAsync(url).Result; + var result = response.Content.ReadAsStringAsync().Result; + Console.WriteLine(result); + } + + [TestMethod] + public void WebApplicationFactory基本用法() + { + var server = new WebApplicationFactory(); + var client = server.CreateClient(); + var url = "demo"; + var response = client.GetAsync(url).Result; + var result = response.Content.ReadAsStringAsync().Result; + Console.WriteLine(result); + } + } +} \ No newline at end of file diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Controllers/DemoController.cs b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Controllers/DemoController.cs new file mode 100644 index 00000000..f28586a7 --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Controllers/DemoController.cs @@ -0,0 +1,33 @@ +using System.Threading; +using Lab.Test.WebApi.Net5.ServiceModels; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Lab.Test.WebApi.Net5.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DemoController : ControllerBase + { + private readonly IFileProvider _fileProvider; + private readonly ILogger _logger; + + public DemoController(ILogger logger, + IFileProvider fileProvider) + { + this._logger = logger; + this._fileProvider = fileProvider; + } + + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(QueryResponse))] + public IActionResult Get(CancellationToken cancel = default) + { + return this.Ok(new QueryResponse + { + Message = this._fileProvider.Name() + }); + } + } +} \ No newline at end of file diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/FileProvider.cs b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/FileProvider.cs new file mode 100644 index 00000000..24f0e1f8 --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/FileProvider.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace Lab.Test.WebApi.Net5 +{ + public class FileProvider:IFileProvider + { + public string Name() + { + return nameof(FileProvider); + } + } +} \ No newline at end of file diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/IFileProvider.cs b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/IFileProvider.cs new file mode 100644 index 00000000..929301aa --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/IFileProvider.cs @@ -0,0 +1,7 @@ +namespace Lab.Test.WebApi.Net5 +{ + public interface IFileProvider + { + string Name(); + } +} \ No newline at end of file diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Lab.Test.WebApi.Net5.csproj b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Lab.Test.WebApi.Net5.csproj new file mode 100644 index 00000000..a3a18f0e --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Lab.Test.WebApi.Net5.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + enable + + + + + + + diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Program.cs b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Program.cs new file mode 100644 index 00000000..3df6d830 --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Program.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Lab.Test.WebApi.Net5 +{ + public class Program + { + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } + + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + } +} \ No newline at end of file diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Properties/launchSettings.json b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Properties/launchSettings.json new file mode 100644 index 00000000..acf4a732 --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:20722", + "sslPort": 44330 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Lab.Test.WebApi.Net5": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/ServiceModels/QueryResponse.cs b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/ServiceModels/QueryResponse.cs new file mode 100644 index 00000000..9d6a0de6 --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/ServiceModels/QueryResponse.cs @@ -0,0 +1,7 @@ +namespace Lab.Test.WebApi.Net5.ServiceModels +{ + public class QueryResponse + { + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Startup.cs b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Startup.cs new file mode 100644 index 00000000..f39a8829 --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/Startup.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; + +namespace Lab.Test.WebApi.Net5 +{ + public class Startup + { + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + { + this.Configuration = configuration; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Lab.Test.WebApi.Net5 v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Lab.Test.WebApi.Net5", Version = "v1" }); + }); + + services.AddScoped(p => new FileProvider()); + services.AddScoped(p => (IFileProvider)p.GetService()); + } + } +} \ No newline at end of file diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/appsettings.Development.json b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/appsettings.json b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.Net5/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.sln b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.sln new file mode 100644 index 00000000..09cca1af --- /dev/null +++ b/WebAPI/Lab.Test.WebApi/Lab.Test.WebApi.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Test.WebApi.Net5", "Lab.Test.WebApi.Net5\Lab.Test.WebApi.Net5.csproj", "{B97FA43E-B196-4800-9AF6-6F5F7B412D1D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Test.WebApi.Net5.TestProject", "Lab.Test.WebApi.Net5.TestProject\Lab.Test.WebApi.Net5.TestProject.csproj", "{B0E91B25-C99E-44D7-9C70-27DF6BF0DBEC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B97FA43E-B196-4800-9AF6-6F5F7B412D1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B97FA43E-B196-4800-9AF6-6F5F7B412D1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B97FA43E-B196-4800-9AF6-6F5F7B412D1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B97FA43E-B196-4800-9AF6-6F5F7B412D1D}.Release|Any CPU.Build.0 = Release|Any CPU + {B0E91B25-C99E-44D7-9C70-27DF6BF0DBEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0E91B25-C99E-44D7-9C70-27DF6BF0DBEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0E91B25-C99E-44D7-9C70-27DF6BF0DBEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0E91B25-C99E-44D7-9C70-27DF6BF0DBEC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git "a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/BasicAuthenticationMiddleware\346\225\264\345\220\210\346\270\254\350\251\246.cs" "b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/BasicAuthenticationMiddleware\346\225\264\345\220\210\346\270\254\350\251\246.cs" new file mode 100644 index 00000000..892e61b5 --- /dev/null +++ "b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/BasicAuthenticationMiddleware\346\225\264\345\220\210\346\270\254\350\251\246.cs" @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest; + +[TestClass] +public class BasicAuthenticationMiddleware整合測試 +{ + [TestMethod] + public void 訪問不需要授權的服務() + { + var server = new TestServer(); + var httpClient = server.CreateClient(); + var url = "test"; + var response = httpClient.GetAsync(url).Result; + var result = response.Content.ReadAsStringAsync().Result; + Console.WriteLine(result); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } + + [TestMethod] + public void 訪問受保護的服務() + { + var server = new TestServer(); + var httpClient = server.CreateClient(); + var url = "protect"; + var clientId = "YAO"; + var clientSecret = "9527"; + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Headers = { Authorization = CreateAuthenticationHeaderValue(clientId, clientSecret) } + }; + + var response = httpClient.SendAsync(request).Result; + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } + + [TestMethod] + public void 訪問受保護的服務_驗證失敗() + { + var server = new TestServer(); + var httpClient = server.CreateClient(); + var url = "protect"; + var clientId = "YAO1234"; + var clientSecret = "9527"; + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Headers = { Authorization = CreateAuthenticationHeaderValue(clientId, clientSecret) } + }; + var response = httpClient.SendAsync(request).Result; + response.Headers.TryGetValues("WWW-Authenticate", out var values); + Console.WriteLine($"驗證失敗:{values.First()}"); + Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); + } + + private static HttpRequestMessage CreateBasicAuthenticationRequest(string url, string clientId, string clientSecret) + { + var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + var authenticationString = $"{clientId}:{clientSecret}"; + var base64Encoded = Convert.ToBase64String(Encoding.ASCII.GetBytes(authenticationString)); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("basic", base64Encoded); + return requestMessage; + } + + private static AuthenticationHeaderValue CreateAuthenticationHeaderValue(string clientId, string clientSecret) + { + var authenticationString = $"{clientId}:{clientSecret}"; + var base64Encoded = Convert.ToBase64String(Encoding.ASCII.GetBytes(authenticationString)); + return new AuthenticationHeaderValue("basic", base64Encoded); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/Models/User.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/Models/User.cs new file mode 100644 index 00000000..d923c451 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/Models/User.cs @@ -0,0 +1,8 @@ +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.Controllers.Models; + +public class User +{ + public string Name { get; set; } + + public int Age { get; set; } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/PermissionController.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/PermissionController.cs new file mode 100644 index 00000000..d45d4f3b --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/PermissionController.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.Controllers.Models; +using Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authorization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.Controllers; + +[ApiController] +[Route("[controller]")] +public class PermissionController : ControllerBase +{ + private readonly ILogger _logger; + + public PermissionController(ILogger logger) + { + this._logger = logger; + } + + [Authorize(Policy = Permission.Operation.Read)] + [HttpGet] + public async Task Get() + { + return this.Ok("好"); + } + + [AllowAnonymous] + [HttpPost] + public async Task Post(User user) + { + return this.Ok("好"); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/ProtectController.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/ProtectController.cs new file mode 100644 index 00000000..b61a4108 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/ProtectController.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.Controllers.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.Controllers; + +[ApiController] +[Route("[controller]")] +public class ProtectController : ControllerBase +{ + private readonly ILogger _logger; + + public ProtectController(ILogger logger) + { + this._logger = logger; + } + + [Authorize] + [HttpGet] + public async Task Get() + { + return this.Ok("好"); + } + + [AllowAnonymous] + [HttpPost] + public async Task Post(User user) + { + return this.Ok("好"); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/TestController.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/TestController.cs new file mode 100644 index 00000000..7d3a574d --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Controllers/TestController.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.Controllers.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.Controllers; + +[ApiController] +[Route("[controller]")] +public class TestController : ControllerBase +{ + private readonly ILogger _logger; + + public TestController(ILogger logger) + { + this._logger = logger; + } + + [AllowAnonymous] + [HttpGet] + public async Task Get() + { + return this.Ok("好"); + } + + [AllowAnonymous] + [HttpPost] + public async Task Post(User user) + { + return this.Ok("好"); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.csproj b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.csproj new file mode 100644 index 00000000..a9c20e84 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + diff --git "a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/PermissionAuthorizationMiddleware\346\225\264\345\220\210\346\270\254\350\251\246.cs" "b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/PermissionAuthorizationMiddleware\346\225\264\345\220\210\346\270\254\350\251\246.cs" new file mode 100644 index 00000000..2ada55db --- /dev/null +++ "b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/PermissionAuthorizationMiddleware\346\225\264\345\220\210\346\270\254\350\251\246.cs" @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.Controllers; +using Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest; + +[TestClass] +public class PermissionAuthorizationMiddleware整合測試 +{ + [TestMethod] + public async Task 訪問受保護的服務_授權成功() + { + var server = CreateTestServer(); + var httpClient = server.CreateClient(); + var url = "permission"; + var clientId = "YAO"; + var clientSecret = "9527"; + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Headers = { Authorization = CreateAuthenticationHeaderValue(clientId, clientSecret) } + }; + var response = httpClient.SendAsync(request).Result; + var content = await response.Content.ReadAsStringAsync(); + Console.WriteLine(content); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } + + [TestMethod] + public async Task 訪問受保護的服務_授權失敗() + { + var server = CreateTestServer(); + var httpClient = server.CreateClient(); + var url = "permission"; + var clientId = "jojo"; + var clientSecret = "9527"; + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Headers = { Authorization = CreateAuthenticationHeaderValue(clientId, clientSecret) } + }; + var response = httpClient.SendAsync(request).Result; + var content = await response.Content.ReadAsStringAsync(); + Console.WriteLine(content); + Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); + } + + private static WebApplicationFactory CreateTestServer() + { + var server = new WebApplicationFactory().WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + services.AddSingleton(); + services.AddControllers() + .AddApplicationPart(typeof(TestController).Assembly); + }); + }); + return server; + } + + private static AuthenticationHeaderValue CreateAuthenticationHeaderValue(string clientId, string clientSecret) + { + var authenticationString = $"{clientId}:{clientSecret}"; + var base64Encoded = Convert.ToBase64String(Encoding.ASCII.GetBytes(authenticationString)); + return new AuthenticationHeaderValue("basic", base64Encoded); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/TestServer.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/TestServer.cs new file mode 100644 index 00000000..b94e4863 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest/TestServer.cs @@ -0,0 +1,24 @@ +using Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.Controllers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest; + +public class TestServer : WebApplicationFactory +{ + private void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddApplicationPart(typeof(TestController).Assembly); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(this.ConfigureServices) + .UseSetting("https_port", "9527") + + // .UseUrls("https://localhost:9527") + ; + } +} \ No newline at end of file diff --git "a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest/BasicAuthenticationHandler\345\226\256\345\205\203\346\270\254\350\251\246.cs" "b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest/BasicAuthenticationHandler\345\226\256\345\205\203\346\270\254\350\251\246.cs" new file mode 100644 index 00000000..aa95c4f2 --- /dev/null +++ "b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest/BasicAuthenticationHandler\345\226\256\345\205\203\346\270\254\350\251\246.cs" @@ -0,0 +1,166 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest; + +[TestClass] +public class BasicAuthenticationHandler單元測試 +{ + [TestMethod] + public async Task 驗證成功() + { + var context = new DefaultHttpContext(); + var authorizationHeader = new StringValues(CreateBasicAuthenticationValue("yao", "9527")); + context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader); + + using var testHost = await CreateTestHost(); + var handler = testHost.Services.GetService(); + await handler.InitializeAsync(new AuthenticationScheme("basic", + "basic", + typeof(BasicAuthenticationHandler)), + context); + var result = await handler.AuthenticateAsync(); + + Assert.IsTrue(result.Succeeded); + } + + [TestMethod] + public async Task 驗證失敗() + { + var context = new DefaultHttpContext(); + var authorizationHeader = new StringValues(string.Empty); + context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader); + + using var testHost = await CreateTestHost(); + var handler = testHost.Services.GetService(); + await handler.InitializeAsync(new AuthenticationScheme("basic", + "basic", + typeof(BasicAuthenticationHandler)), + context); + var result = await handler.AuthenticateAsync(); + + Assert.IsFalse(result.Succeeded); + Assert.AreEqual("Invalid authorization Header", result.Failure.Message); + } + + [TestMethod] + public async Task 驗證失敗後回應錯誤() + { + var context = new DefaultHttpContext + { + Response = { Body = new MemoryStream() } + }; + + var authorizationHeader = new StringValues(CreateBasicAuthenticationValue("yao123", "9527")); + context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader); + + using var testHost = await CreateTestHost(); + var handler = testHost.Services.GetService(); + await handler.InitializeAsync(new AuthenticationScheme("basic", + "basic", + typeof(BasicAuthenticationHandler)), + context); + var authenticateResult = await handler.AuthenticateAsync(); + await handler.ChallengeAsync(authenticateResult.Properties); + var response = context.Response; + + Assert.IsFalse(authenticateResult.Succeeded); + var expected = "Basic realm=\"Demo Site\", charset=\"UTF-8\""; + Assert.AreEqual(expected, response.Headers.WWWAuthenticate.ToString()); + } + + private static string CreateBasicAuthenticationValue(string userId, string password) + { + var certificate = $"{userId}:{password}"; + var base64Encode = Convert.ToBase64String(Encoding.ASCII.GetBytes(certificate)); + return $"Basic {base64Encode}"; + } + + private static async Task CreateTestClient() + { + var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseTestServer() + .ConfigureServices( + services => + { + services.AddSingleton(); + services.AddBasicAuthentication(_ => { }); + services.AddAuthorization(); + }) + .Configure(app => + { + app.UseAuthentication(); + app.UseAuthorization(); + }); + }) + .StartAsync(); + + return host.GetTestClient(); + } + + private static async Task CreateTestHost() + { + var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseTestServer() + .ConfigureServices( + services => + { + services.AddSingleton(); + services.AddBasicAuthentication(_ => { }); + services.AddAuthorization(); + }) + .Configure(app => + { + app.UseAuthentication(); + app.UseAuthorization(); + }); + }) + .StartAsync(); + return host; + } + + private static async Task CreateTestServer() + { + var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseTestServer() + .ConfigureServices( + services => + { + services.AddSingleton(); + services.AddBasicAuthentication(_ => { }); + services.AddAuthorization(); + }) + .Configure(app => + { + app.UseAuthentication(); + app.UseAuthorization(); + }); + }) + .StartAsync(); + + var server = host.GetTestServer(); + server.BaseAddress = new Uri("https://我真的是假的/不要打我的臉/"); + return server; + } +} \ No newline at end of file diff --git "a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest/BasicAuthenticationMiddleware\345\226\256\345\205\203\346\270\254\350\251\246.cs" "b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest/BasicAuthenticationMiddleware\345\226\256\345\205\203\346\270\254\350\251\246.cs" new file mode 100644 index 00000000..91941d36 --- /dev/null +++ "b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest/BasicAuthenticationMiddleware\345\226\256\345\205\203\346\270\254\350\251\246.cs" @@ -0,0 +1,75 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest; + +[TestClass] +public class BasicAuthenticationMiddleware單元測試 +{ + [TestMethod] + public async Task 驗證失敗() + { + using var server = await CreateTestServer(); + var httpContext = await server.SendAsync(config => + { + config.Request.Headers.Authorization = CreateBasicAuthenticationValue("yao", "9527xxxx"); + }); + + // 驗證失敗沒有觸發 BasicAuthenticationHandler.HandleChallengeAsync + var userPrincipal = httpContext.User; + Assert.AreEqual(false, userPrincipal.Identity.IsAuthenticated); + } + + [TestMethod] + public async Task 驗證成功() + { + using var server = await CreateTestServer(); + var httpContext = await server.SendAsync(config => + { + config.Request.Headers.Authorization = CreateBasicAuthenticationValue("yao", "9527"); + }); + var userPrincipal = httpContext.User; + Assert.AreEqual(true, userPrincipal.Identity.IsAuthenticated); + } + + private static string CreateBasicAuthenticationValue(string userId, string password) + { + var certificate = $"{userId}:{password}"; + var base64Encode = Convert.ToBase64String(Encoding.ASCII.GetBytes(certificate)); + return $"Basic {base64Encode}"; + } + + private static async Task CreateTestServer() + { + var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseTestServer() + .ConfigureServices( + services => + { + services.AddSingleton(); + services.AddBasicAuthentication(_ => { }); + services.AddAuthorization(); + }) + .Configure(app => + { + app.UseAuthentication(); + app.UseAuthorization(); + }); + }) + .StartAsync(); + + var server = host.GetTestServer(); + server.BaseAddress = new Uri("https://我真的是假的/不要打我的臉/"); + return server; + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest.csproj b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest.csproj new file mode 100644 index 00000000..65a60277 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest/Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Controllers/DemoController.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Controllers/DemoController.cs new file mode 100644 index 00000000..65298d94 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Controllers/DemoController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Controllers; + +[ApiController] +[Route("[controller]")] +public class DemoController : ControllerBase +{ + private readonly ILogger _logger; + + public DemoController(ILogger logger) + { + this._logger = logger; + } + + [AllowAnonymous] + [HttpGet] + public async Task Get() + { + return this.Ok("好"); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/FieldTypeAssistant.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/FieldTypeAssistant.cs new file mode 100644 index 00000000..a40d46a2 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/FieldTypeAssistant.cs @@ -0,0 +1,41 @@ +using System.Collections.Concurrent; +using System.Reflection; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite; + +public class FieldTypeAssistant +{ + private static ConcurrentDictionary> s_fieldTypeList = new(); + + public static Dictionary GetEnumValues() + { + return Enum.GetValues(typeof(T)) + .Cast() + .ToDictionary(p => p.ToString(), p => p); + } + + public static Dictionary GetStaticFieldName() + { + var type = typeof(T); + var fieldTypeList = s_fieldTypeList; + if (fieldTypeList.TryGetValue(type, out var results)) + { + return results; + } + + var bindingFlags = BindingFlags.Public + | BindingFlags.Static + ; + results = new Dictionary(); + var fieldInfosInfos = type.GetFields(bindingFlags); + foreach (var fieldInfo in fieldInfosInfos) + { + var value = fieldInfo.GetValue(null); + + results.Add(value.ToString(), fieldInfo.FieldType); + } + + fieldTypeList.TryAdd(type, results); + return results; + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Lab.AspNetCore.Security.BasicAuthenticationSite.csproj b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Lab.AspNetCore.Security.BasicAuthenticationSite.csproj new file mode 100644 index 00000000..e71b3ef4 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Lab.AspNetCore.Security.BasicAuthenticationSite.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Program.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Program.cs new file mode 100644 index 00000000..26a528f4 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Program.cs @@ -0,0 +1,63 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Unicode; +using Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; +using Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authorization; +using Microsoft.AspNetCore.Authorization; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Logging.AddConsole(); + +// builder.Services.AddAuthentication(BasicAuthenticationDefaults.AuthenticationScheme) +// .AddScheme(BasicAuthenticationDefaults.AuthenticationScheme, +// p => new BasicAuthenticationOptions()); +builder.Services.AddSingleton(p=>new JsonSerializerOptions +{ + Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs), + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, +}); +builder.Services.AddSingleton(); +builder.Services.AddBasicAuthentication(options => { }); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +// builder.Services.AddAuthorization(options => +// { +// options.AddPolicy("Permission", policy => +// policy.Requirements.Add(new PermissionAuthorizationRequirement())); +// }); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); + +public partial class Program +{ +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Properties/launchSettings.json b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Properties/launchSettings.json new file mode 100644 index 00000000..4ed84fd8 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:20169", + "sslPort": 44329 + } + }, + "profiles": { + "Lab.AspNetCore.Security.BasicAuthenticationSite": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7089;http://localhost:5089", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationDefaults.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationDefaults.cs new file mode 100644 index 00000000..ccd37f3f --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationDefaults.cs @@ -0,0 +1,6 @@ +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; + +public static class BasicAuthenticationDefaults +{ + public const string AuthenticationScheme = "Basic"; +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationExtensions.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationExtensions.cs new file mode 100644 index 00000000..267ade4f --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Authentication; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; + +public static class BasicAuthenticationExtensions +{ + public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, + Action configureOptions) + { + return builder.AddScheme( + BasicAuthenticationDefaults.AuthenticationScheme, + BasicAuthenticationDefaults.AuthenticationScheme, configureOptions); + } + + public static AuthenticationBuilder AddBasicAuthentication(this IServiceCollection services, + Action configureOptions) + { + return services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = BasicAuthenticationDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = BasicAuthenticationDefaults.AuthenticationScheme; + }) + .AddBasic(configureOptions); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationHandler.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationHandler.cs new file mode 100644 index 00000000..41f3d02b --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationHandler.cs @@ -0,0 +1,123 @@ +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; + +public class BasicAuthenticationHandler : AuthenticationHandler +{ + private readonly IBasicAuthenticationProvider _authenticationProvider; + private string _failReason; + + public BasicAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + IBasicAuthenticationProvider authenticationProvider) + : base(options, logger, encoder, clock) + { + this._authenticationProvider = authenticationProvider; + } + + /// + /// + /// + void CreateTestServer() + { + + } + + void CreateTask() + { + + } + protected override async Task HandleAuthenticateAsync() + { + var schemeName = this.Scheme.Name; //由外部注入 + var endpoint = this.Context.GetEndpoint(); + if (endpoint?.Metadata?.GetMetadata() != null) + { + return AuthenticateResult.NoResult(); + } + + if (!this.Request.Headers.ContainsKey(HeaderNames.Authorization)) + { + this._failReason = "Invalid basic authentication header"; + return AuthenticateResult.Fail(this._failReason); + } + + if (!AuthenticationHeaderValue.TryParse(this.Request.Headers[HeaderNames.Authorization], + out var authHeaderValue)) + { + this._failReason = "Invalid authorization Header"; + return AuthenticateResult.Fail(this._failReason); + } + + if (authHeaderValue.Scheme.StartsWith(schemeName, StringComparison.InvariantCultureIgnoreCase) == false) + { + this._failReason = "Invalid authorization scheme name"; + return AuthenticateResult.Fail("Invalid authorization scheme name"); + } + + var credentialBytes = Convert.FromBase64String(authHeaderValue.Parameter); + var userAndPassword = Encoding.UTF8.GetString(credentialBytes); + var credentials = userAndPassword.Split(':'); + if (credentials.Length != 2) + { + this._failReason = "Invalid basic authentication header"; + return AuthenticateResult.Fail(this._failReason); + } + + var user = credentials[0]; + var password = credentials[1]; + + var isValidate = await this._authenticationProvider.IsValidateAsync(user, password, CancellationToken.None); + + if (!isValidate) + { + this._failReason = "Invalid username or password"; + return AuthenticateResult.Fail(this._failReason); + } + + return this.SignIn(user); + } + + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + // 寫入詳細的失敗原因,排除敏感性資料 + this.Logger.LogInformation("{FailureReason}", new + { + Code = "InvalidAuthentication", + Message = this._failReason + }); + + this.Response.StatusCode = 401; + this.Response.HttpContext.Features.Get().ReasonPhrase = this._failReason; + this.Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{this.Options.Realm}\", charset=\"UTF-8\""; + + // 響應粗糙的內容,這不是標準的 Basic Authentication 失敗的回傳,僅是為了示意 + this.Response.WriteAsJsonAsync(new + { + Code = "InvalidAuthentication", + Message = "Please contact your administrator" + }); + await Task.CompletedTask; + } + + private AuthenticateResult SignIn(string user) + { + var schemeName = this.Scheme.Name; + var claims = new[] { new Claim(ClaimTypes.Name, user) }; + var identity = new ClaimsIdentity(claims, schemeName); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, schemeName); + return AuthenticateResult.Success(ticket); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationOptions.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationOptions.cs new file mode 100644 index 00000000..735e435f --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationOptions.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authentication; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; + +public class BasicAuthenticationOptions : AuthenticationSchemeOptions +{ + public string Realm { get; set; } = "Demo Site"; +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationPostConfigureOptions.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationPostConfigureOptions.cs new file mode 100644 index 00000000..40c7a136 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationPostConfigureOptions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Options; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; + +public class BasicAuthenticationPostConfigureOptions : IPostConfigureOptions +{ + public void PostConfigure(string name, BasicAuthenticationOptions options) + { + if (string.IsNullOrEmpty(options.Realm)) + { + throw new InvalidOperationException("Realm must be provided in options"); + } + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationProvider.cs new file mode 100644 index 00000000..38de0579 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationProvider.cs @@ -0,0 +1,24 @@ +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; + +public class BasicAuthenticationProvider : IBasicAuthenticationProvider +{ + private readonly Dictionary _clientIdentities = new(StringComparer.InvariantCultureIgnoreCase) + { + { "yao", "9527" } + }; + + public Task IsValidateAsync(string user, string password, CancellationToken cancel = default) + { + if (this._clientIdentities.TryGetValue(user, out var secret) == false) + { + return Task.FromResult(false); + } + + if (password != secret) + { + return Task.FromResult(false); + } + + return Task.FromResult(true); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/DefaultBasicAuthenticationProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/DefaultBasicAuthenticationProvider.cs new file mode 100644 index 00000000..b90712b5 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/DefaultBasicAuthenticationProvider.cs @@ -0,0 +1,9 @@ +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; + +public class DefaultBasicAuthenticationProvider : IBasicAuthenticationProvider +{ + public Task IsValidateAsync(string user, string password, CancellationToken cancel = default) + { + return Task.FromResult(true); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/IBasicAuthenticationProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/IBasicAuthenticationProvider.cs new file mode 100644 index 00000000..a5c4d00a --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/IBasicAuthenticationProvider.cs @@ -0,0 +1,6 @@ +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; + +public interface IBasicAuthenticationProvider +{ + Task IsValidateAsync(string user, string password, CancellationToken cancel); +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/IPermissionAuthorizationProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/IPermissionAuthorizationProvider.cs new file mode 100644 index 00000000..a6fa3b2a --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/IPermissionAuthorizationProvider.cs @@ -0,0 +1,6 @@ +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authorization; + +public interface IPermissionAuthorizationProvider +{ + IEnumerable GetPermissions(string userId); +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/Permission.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/Permission.cs new file mode 100644 index 00000000..5af9af80 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/Permission.cs @@ -0,0 +1,22 @@ +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authorization; + +public class Permission +{ + public class Operation + { + public const string Write = $"{nameof(Permission)}.{nameof(Operation)}:{nameof(Write)}"; + public const string Read = $"{nameof(Permission)}.{nameof(Operation)}:{nameof(Read)}"; + + private static readonly Lazy> s_values + = new(() => + { + return FieldTypeAssistant.GetStaticFieldName() + .ToDictionary(p => p.Key, + p => p.Value, + StringComparer.InvariantCultureIgnoreCase); + }); + + public static Dictionary GetValues() + => s_values.Value; + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationHandler.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationHandler.cs new file mode 100644 index 00000000..fed3f092 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationHandler.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authorization; + +public class PermissionAuthorizationHandler : AuthorizationHandler +{ + private readonly IPermissionAuthorizationProvider _authorizationProvider; + + public PermissionAuthorizationHandler(IPermissionAuthorizationProvider authorizationProvider) + { + this._authorizationProvider = authorizationProvider; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + PermissionAuthorizationRequirement requirement) + { + if (context.User.Identity.IsAuthenticated == false) + { + context.Fail(new AuthorizationFailureReason(this, $"目前請求沒有通過驗證")); + return; + } + + var userId = context.User.Identity.Name; + var permissions = this._authorizationProvider.GetPermissions(userId); + if (permissions.Any(p => p.StartsWith(requirement.PolicyName, StringComparison.InvariantCultureIgnoreCase)) == + false) + { + context.Fail(new AuthorizationFailureReason(this, $"用戶 '{userId}',沒有授權 '{requirement.PolicyName}'")); + } + + if (context.HasFailed == false) + { + context.Succeed(requirement); + } + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationMiddlewareResultHandler.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationMiddlewareResultHandler.cs new file mode 100644 index 00000000..03e594a0 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationMiddlewareResultHandler.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Policy; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authorization; + +public class PermissionAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler +{ + private readonly ILogger _logger; + + private readonly JsonSerializerOptions _jsonSerializerOptions; + + public PermissionAuthorizationMiddlewareResultHandler( + ILogger logger, + JsonSerializerOptions jsonSerializerOptions) + { + this._logger = logger; + this._jsonSerializerOptions = jsonSerializerOptions; + } + + public async Task HandleAsync( + RequestDelegate next, + HttpContext context, + AuthorizationPolicy policy, + PolicyAuthorizationResult authorizeResult) + { + var permissionAuthorizationRequirements = policy.Requirements.OfType(); + + if (authorizeResult.Forbidden + && permissionAuthorizationRequirements.Any()) + { + context.Response.StatusCode = 403; + this._logger.LogInformation("{AuthorizationFailureResults}", new + { + ErrorCode = "Invalid Authorization", + ErrorMessages = authorizeResult.AuthorizationFailure.FailureReasons + }); + + // 回傳前端模糊訊息 + await context.Response.WriteAsJsonAsync(new + { + ErrorCode = "Invalid Authorization", + ErrorMessages = new[] { "Please contact your administrator" } + // ErrorMessages = authorizeResult.AuthorizationFailure.FailureReasons + }, this._jsonSerializerOptions); + return; + } + + await next.Invoke(context); + + // await next(context); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationPolicyProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationPolicyProvider.cs new file mode 100644 index 00000000..774f1094 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationPolicyProvider.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authorization; + +internal class PermissionAuthorizationPolicyProvider : IAuthorizationPolicyProvider +{ + public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; } + + public PermissionAuthorizationPolicyProvider(IOptions options) + { + // ASP.NET Core only uses one authorization policy provider, so if the custom implementation + // doesn't handle all policies (including default policies, etc.) it should fall back to an + // alternate provider. + // + // In this sample, a default authorization policy provider (constructed with options from the + // dependency injection container) is used if this custom provider isn't able to handle a given + // policy name. + // + // If a custom policy provider is able to handle all expected policy names then, of course, this + // fallback pattern is unnecessary. + FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options); + } + + public Task GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync(); + + public Task GetFallbackPolicyAsync() => FallbackPolicyProvider.GetFallbackPolicyAsync(); + + // Policies are looked up by string name, so expect 'parameters' (like age) + // to be embedded in the policy names. This is abstracted away from developers + // by the more strongly-typed attributes derived from AuthorizeAttribute + // (like [MinimumAgeAuthorize] in this sample) + public Task GetPolicyAsync(string policyName) + { + var operationValues = Permission.Operation.GetValues(); + if (operationValues.Any(p => p.Key.StartsWith(policyName, StringComparison.InvariantCultureIgnoreCase))) + { + var policy = new AuthorizationPolicyBuilder(); + policy.AddRequirements(new PermissionAuthorizationRequirement + { + PolicyName = policyName + }); + return Task.FromResult(policy.Build()); + } + + // If the policy name doesn't match the format expected by this policy provider, + // try the fallback provider. If no fallback provider is used, this would return + // Task.FromResult(null) instead. + return FallbackPolicyProvider.GetPolicyAsync(policyName); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationProvider.cs new file mode 100644 index 00000000..aa339fb4 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationProvider.cs @@ -0,0 +1,21 @@ +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authorization; + +public class PermissionAuthorizationProvider : IPermissionAuthorizationProvider +{ + private readonly Dictionary> _clientPermissions = + new(StringComparer.InvariantCultureIgnoreCase) + { + { "yao", new[] { Permission.Operation.Read, Permission.Operation.Write } }, + { "jojo", new[] { Permission.Operation.Read} } + }; + + public IEnumerable GetPermissions(string userId) + { + if (this._clientPermissions.TryGetValue(userId, out var result) == false) + { + result = new List(); + } + + return result; + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationRequirement.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationRequirement.cs new file mode 100644 index 00000000..61888f90 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationRequirement.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authorization; + +public class PermissionAuthorizationRequirement : IAuthorizationRequirement +{ + public string PolicyName { get; init; } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/appsettings.Development.json b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/appsettings.json b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.sln b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.sln new file mode 100644 index 00000000..1fc06b88 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.AspNetCore.Security.BasicAuthenticationSite", "Lab.AspNetCore.Security.BasicAuthenticationSite\Lab.AspNetCore.Security.BasicAuthenticationSite.csproj", "{C2FC8F88-DAD8-4677-8C5A-A9353C5353D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest", "Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest\Lab.AspNetCore.Security.BasicAuthenticationSite.IntegrateTest.csproj", "{13085C3E-F174-45D2-B8F7-3EE51D42DDF4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest", "Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest\Lab.AspNetCore.Security.BasicAuthenticationSite.UnitTest.csproj", "{12F00FC6-1D31-48CD-AB17-B00F76846A33}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C2FC8F88-DAD8-4677-8C5A-A9353C5353D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2FC8F88-DAD8-4677-8C5A-A9353C5353D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2FC8F88-DAD8-4677-8C5A-A9353C5353D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2FC8F88-DAD8-4677-8C5A-A9353C5353D7}.Release|Any CPU.Build.0 = Release|Any CPU + {13085C3E-F174-45D2-B8F7-3EE51D42DDF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13085C3E-F174-45D2-B8F7-3EE51D42DDF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13085C3E-F174-45D2-B8F7-3EE51D42DDF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13085C3E-F174-45D2-B8F7-3EE51D42DDF4}.Release|Any CPU.Build.0 = Release|Any CPU + {12F00FC6-1D31-48CD-AB17-B00F76846A33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12F00FC6-1D31-48CD-AB17-B00F76846A33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12F00FC6-1D31-48CD-AB17-B00F76846A33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12F00FC6-1D31-48CD-AB17-B00F76846A33}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebAPI/Swagger/Lab.SpecFirst/Lab.SpecFirst.sln b/WebAPI/Swagger/Lab.SpecFirst/Lab.SpecFirst.sln new file mode 100644 index 00000000..9b8579aa --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/Lab.SpecFirst.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{1C8C1BD9-1338-47A0-963B-D35B1AD07476}" + ProjectSection(SolutionItems) = preProject + Taskfile.yml = Taskfile.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "doc", "doc", "{7C405A84-132F-43F2-9F97-388CC40BED1D}" + ProjectSection(SolutionItems) = preProject + doc\index.yaml = doc\index.yaml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F900DCE0-EF45-4629-AA24-949E65BAB714}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.SpecFirst.Web", "src\Lab.SpecFirst.Web\Lab.SpecFirst.Web.csproj", "{70299524-F9F4-41DF-80EF-D1CE03C2965A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.SpecFirst.Adapter", "src\Lab.SpecFirst.Adapter\Lab.SpecFirst.Adapter.csproj", "{F5AAACC5-781F-41E6-8CD5-39389A00942C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {70299524-F9F4-41DF-80EF-D1CE03C2965A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70299524-F9F4-41DF-80EF-D1CE03C2965A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70299524-F9F4-41DF-80EF-D1CE03C2965A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70299524-F9F4-41DF-80EF-D1CE03C2965A}.Release|Any CPU.Build.0 = Release|Any CPU + {F5AAACC5-781F-41E6-8CD5-39389A00942C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5AAACC5-781F-41E6-8CD5-39389A00942C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5AAACC5-781F-41E6-8CD5-39389A00942C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5AAACC5-781F-41E6-8CD5-39389A00942C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {7C405A84-132F-43F2-9F97-388CC40BED1D} = {1C8C1BD9-1338-47A0-963B-D35B1AD07476} + {70299524-F9F4-41DF-80EF-D1CE03C2965A} = {F900DCE0-EF45-4629-AA24-949E65BAB714} + {F5AAACC5-781F-41E6-8CD5-39389A00942C} = {F900DCE0-EF45-4629-AA24-949E65BAB714} + EndGlobalSection +EndGlobal diff --git a/WebAPI/Swagger/Lab.SpecFirst/Taskfile.yml b/WebAPI/Swagger/Lab.SpecFirst/Taskfile.yml new file mode 100644 index 00000000..61cb3a6a --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/Taskfile.yml @@ -0,0 +1,20 @@ +version: "3" + +dotenv: [ "secrets/secrets.env" ] + +tasks: + rest-codegen-code: + desc: 產生 Client / Server Code + cmds: + - task: rest-codegen-client + - task: rest-codegen-server + + rest-codegen-client: + desc: 產生 Client Code + cmds: + - nswag openapi2csclient /input:doc/index.yaml /classname:LabSpecClient /namespace:Lab.SpecFirst.Adapter /output:src/Lab.SpecFirst.Adapter/AutoGenerated/LabSpecClient.cs /jsonLibrary:SystemTextJson /generateClientInterfaces:true /exposeJsonSerializerSettings:false /useBaseUrl:false + + rest-codegen-server: + desc: 產生 Server Code + cmds: + - nswag openapi2cscontroller /input:doc/index.yaml /classname:SpecFirstContract /namespace:Lab.SpecFirst.Web.Controllers /output:src/Lab.SpecFirst.Web/AutoGenerated/Controller.cs /jsonLibrary:SystemTextJson diff --git a/WebAPI/Swagger/Lab.SpecFirst/doc/index.yaml b/WebAPI/Swagger/Lab.SpecFirst/doc/index.yaml new file mode 100644 index 00000000..565bfc49 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/doc/index.yaml @@ -0,0 +1,112 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://localhost:7087/api/ +# - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Adapter/AutoGenerated/LabSpecClient.cs b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Adapter/AutoGenerated/LabSpecClient.cs new file mode 100644 index 00000000..55ae054c --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Adapter/AutoGenerated/LabSpecClient.cs @@ -0,0 +1,531 @@ +//---------------------- +// +// Generated using the NSwag toolchain v13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" + +namespace Lab.SpecFirst.Adapter +{ + using System = global::System; + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + public partial interface ILabSpecClient + { + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + /// A server side error occurred. + System.Threading.Tasks.Task> ListPetsAsync(int? limit); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + /// A server side error occurred. + System.Threading.Tasks.Task> ListPetsAsync(int? limit, System.Threading.CancellationToken cancellationToken); + + /// Create a pet + /// Null response + /// A server side error occurred. + System.Threading.Tasks.Task CreatePetsAsync(); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Create a pet + /// Null response + /// A server side error occurred. + System.Threading.Tasks.Task CreatePetsAsync(System.Threading.CancellationToken cancellationToken); + + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + /// A server side error occurred. + System.Threading.Tasks.Task ShowPetByIdAsync(string petId); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + /// A server side error occurred. + System.Threading.Tasks.Task ShowPetByIdAsync(string petId, System.Threading.CancellationToken cancellationToken); + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class LabSpecClient : ILabSpecClient + { + private System.Net.Http.HttpClient _httpClient; + private System.Lazy _settings; + + public LabSpecClient(System.Net.Http.HttpClient httpClient) + { + _httpClient = httpClient; + _settings = new System.Lazy(CreateSerializerSettings); + } + + private System.Text.Json.JsonSerializerOptions CreateSerializerSettings() + { + var settings = new System.Text.Json.JsonSerializerOptions(); + UpdateJsonSerializerSettings(settings); + return settings; + } + + protected System.Text.Json.JsonSerializerOptions JsonSerializerSettings { get { return _settings.Value; } } + + partial void UpdateJsonSerializerSettings(System.Text.Json.JsonSerializerOptions settings); + + + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url); + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder); + partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response); + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + /// A server side error occurred. + public System.Threading.Tasks.Task> ListPetsAsync(int? limit) + { + return ListPetsAsync(limit, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + /// A server side error occurred. + public async System.Threading.Tasks.Task> ListPetsAsync(int? limit, System.Threading.CancellationToken cancellationToken) + { + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append("pets?"); + if (limit != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("limit") + "=").Append(System.Uri.EscapeDataString(ConvertToString(limit, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + urlBuilder_.Length--; + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("unexpected error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// Create a pet + /// Null response + /// A server side error occurred. + public System.Threading.Tasks.Task CreatePetsAsync() + { + return CreatePetsAsync(System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Create a pet + /// Null response + /// A server side error occurred. + public async System.Threading.Tasks.Task CreatePetsAsync(System.Threading.CancellationToken cancellationToken) + { + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append("pets"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 201) + { + return; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("unexpected error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + /// A server side error occurred. + public System.Threading.Tasks.Task ShowPetByIdAsync(string petId) + { + return ShowPetByIdAsync(petId, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + /// A server side error occurred. + public async System.Threading.Tasks.Task ShowPetByIdAsync(string petId, System.Threading.CancellationToken cancellationToken) + { + if (petId == null) + throw new System.ArgumentNullException("petId"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append("pets/{petId}"); + urlBuilder_.Replace("{petId}", System.Uri.EscapeDataString(ConvertToString(petId, System.Globalization.CultureInfo.InvariantCulture))); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("unexpected error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + protected struct ObjectResponseResult + { + public ObjectResponseResult(T responseObject, string responseText) + { + this.Object = responseObject; + this.Text = responseText; + } + + public T Object { get; } + + public string Text { get; } + } + + public bool ReadResponseAsString { get; set; } + + protected virtual async System.Threading.Tasks.Task> ReadObjectResponseAsync(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Threading.CancellationToken cancellationToken) + { + if (response == null || response.Content == null) + { + return new ObjectResponseResult(default(T), string.Empty); + } + + if (ReadResponseAsString) + { + var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + try + { + var typedBody = System.Text.Json.JsonSerializer.Deserialize(responseText, JsonSerializerSettings); + return new ObjectResponseResult(typedBody, responseText); + } + catch (System.Text.Json.JsonException exception) + { + var message = "Could not deserialize the response body string as " + typeof(T).FullName + "."; + throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception); + } + } + else + { + try + { + using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + var typedBody = await System.Text.Json.JsonSerializer.DeserializeAsync(responseStream, JsonSerializerSettings, cancellationToken).ConfigureAwait(false); + return new ObjectResponseResult(typedBody, string.Empty); + } + } + catch (System.Text.Json.JsonException exception) + { + var message = "Could not deserialize the response body stream as " + typeof(T).FullName + "."; + throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception); + } + } + } + + private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo) + { + if (value == null) + { + return ""; + } + + if (value is System.Enum) + { + var name = System.Enum.GetName(value.GetType(), value); + if (name != null) + { + var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name); + if (field != null) + { + var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute)) + as System.Runtime.Serialization.EnumMemberAttribute; + if (attribute != null) + { + return attribute.Value != null ? attribute.Value : name; + } + } + + var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo)); + return converted == null ? string.Empty : converted; + } + } + else if (value is bool) + { + return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant(); + } + else if (value is byte[]) + { + return System.Convert.ToBase64String((byte[]) value); + } + else if (value.GetType().IsArray) + { + var array = System.Linq.Enumerable.OfType((System.Array) value); + return string.Join(",", System.Linq.Enumerable.Select(array, o => ConvertToString(o, cultureInfo))); + } + + var result = System.Convert.ToString(value, cultureInfo); + return result == null ? "" : result; + } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Pet + { + [System.Text.Json.Serialization.JsonPropertyName("id")] + public long Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("name")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Name { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("tag")] + public string Tag { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties = new System.Collections.Generic.Dictionary(); + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties; } + set { _additionalProperties = value; } + } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Pets : System.Collections.ObjectModel.Collection + { + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Error + { + [System.Text.Json.Serialization.JsonPropertyName("code")] + public int Code { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("message")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Message { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties = new System.Collections.Generic.Dictionary(); + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties; } + set { _additionalProperties = value; } + } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class ApiException : System.Exception + { + public int StatusCode { get; private set; } + + public string Response { get; private set; } + + public System.Collections.Generic.IReadOnlyDictionary> Headers { get; private set; } + + public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Exception innerException) + : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException) + { + StatusCode = statusCode; + Response = response; + Headers = headers; + } + + public override string ToString() + { + return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString()); + } + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class ApiException : ApiException + { + public TResult Result { get; private set; } + + public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary> headers, TResult result, System.Exception innerException) + : base(message, statusCode, response, headers, innerException) + { + Result = result; + } + } + +} + +#pragma warning restore 1591 +#pragma warning restore 1573 +#pragma warning restore 472 +#pragma warning restore 114 +#pragma warning restore 108 +#pragma warning restore 3016 \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Adapter/Lab.SpecFirst.Adapter.csproj b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Adapter/Lab.SpecFirst.Adapter.csproj new file mode 100644 index 00000000..eb2460e9 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Adapter/Lab.SpecFirst.Adapter.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/.dockerignore b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/AutoGenerated/Controller.cs b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/AutoGenerated/Controller.cs new file mode 100644 index 00000000..c458dd95 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/AutoGenerated/Controller.cs @@ -0,0 +1,137 @@ +//---------------------- +// +// Generated using the NSwag toolchain v13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" + +namespace Lab.SpecFirst.Web.Controllers +{ + using System = global::System; + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + public interface ISpecFirstContractController + { + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + System.Threading.Tasks.Task> ListPetsAsync(int? limit); + + /// Create a pet + /// Null response + System.Threading.Tasks.Task CreatePetsAsync(); + + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + System.Threading.Tasks.Task ShowPetByIdAsync(string petId); + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + [Microsoft.AspNetCore.Mvc.Route("api/")] + public partial class SpecFirstContractController : Microsoft.AspNetCore.Mvc.ControllerBase + { + private ISpecFirstContractController _implementation; + + public SpecFirstContractController(ISpecFirstContractController implementation) + { + _implementation = implementation; + } + + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("pets")] + public System.Threading.Tasks.Task> ListPets([Microsoft.AspNetCore.Mvc.FromQuery] int? limit) + { + return _implementation.ListPetsAsync(limit); + } + + /// Create a pet + /// Null response + [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("pets")] + public System.Threading.Tasks.Task CreatePets() + { + return _implementation.CreatePetsAsync(); + } + + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("pets/{petId}")] + public System.Threading.Tasks.Task ShowPetById(string petId) + { + return _implementation.ShowPetByIdAsync(petId); + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Pet + { + [System.Text.Json.Serialization.JsonPropertyName("id")] + public long Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("name")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Name { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("tag")] + public string Tag { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties = new System.Collections.Generic.Dictionary(); + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties; } + set { _additionalProperties = value; } + } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Pets : System.Collections.ObjectModel.Collection + { + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Error + { + [System.Text.Json.Serialization.JsonPropertyName("code")] + public int Code { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("message")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Message { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties = new System.Collections.Generic.Dictionary(); + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties; } + set { _additionalProperties = value; } + } + + + } + +} + +#pragma warning restore 1591 +#pragma warning restore 1573 +#pragma warning restore 472 +#pragma warning restore 114 +#pragma warning restore 108 +#pragma warning restore 3016 \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Controllers/SpecFirstController.cs b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Controllers/SpecFirstController.cs new file mode 100644 index 00000000..eed7b89a --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Controllers/SpecFirstController.cs @@ -0,0 +1,38 @@ +namespace Lab.SpecFirst.Web.Controllers; + +class SpecFirstController : ISpecFirstContractController +{ + public async Task> ListPetsAsync(int? limit) + { + return new List() + { + new() + { + Id = 1, + Name = "yao", + Tag = "dog", + AdditionalProperties = new Dictionary() + { + } + }, + }; + } + + public async Task CreatePetsAsync() + { + + } + + public async Task ShowPetByIdAsync(string petId) + { + return new() + { + Id = 1, + Name = "yao", + Tag = "dog", + AdditionalProperties = new Dictionary() + { + } + }; + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Controllers/WeatherForecastController.cs b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..f0631a40 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.SpecFirst.Web.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateTime.Now.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Dockerfile b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Dockerfile new file mode 100644 index 00000000..e568de25 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["src/Lab.SpecFirst.Web/Lab.SpecFirst.Web.csproj", "Lab.SpecFirst.Web/"] +RUN dotnet restore "src/Lab.SpecFirst.Web/Lab.SpecFirst.Web.csproj" +COPY . . +WORKDIR "/src/Lab.SpecFirst.Web" +RUN dotnet build "Lab.SpecFirst.Web.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Lab.SpecFirst.Web.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Lab.SpecFirst.Web.dll"] diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Lab.SpecFirst.Web.csproj b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Lab.SpecFirst.Web.csproj new file mode 100644 index 00000000..0db9dec4 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Lab.SpecFirst.Web.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + Linux + + + + + + + diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Program.cs b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Program.cs new file mode 100644 index 00000000..8f8bd1ae --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Program.cs @@ -0,0 +1,29 @@ +using Lab.SpecFirst.Web.Controllers; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddScoped(); + +// Add services to the container. +builder.Services.AddControllers(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Properties/launchSettings.json b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Properties/launchSettings.json new file mode 100644 index 00000000..466bd7be --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:9860", + "sslPort": 44313 + } + }, + "profiles": { + "Lab.SpecFirst.Web": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7041;http://localhost:5041", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/WeatherForecast.cs b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/WeatherForecast.cs new file mode 100644 index 00000000..bacb51a3 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Lab.SpecFirst.Web; + +public class WeatherForecast +{ + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/appsettings.Development.json b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/appsettings.json b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst/src/Lab.SpecFirst.Web/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/WebAPI/Swagger/Lab.SpecFirst2/Lab.SpecFirst.sln b/WebAPI/Swagger/Lab.SpecFirst2/Lab.SpecFirst.sln new file mode 100644 index 00000000..37c434a3 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/Lab.SpecFirst.sln @@ -0,0 +1,51 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{1C8C1BD9-1338-47A0-963B-D35B1AD07476}" + ProjectSection(SolutionItems) = preProject + Taskfile.yml = Taskfile.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "doc", "doc", "{7C405A84-132F-43F2-9F97-388CC40BED1D}" + ProjectSection(SolutionItems) = preProject + doc\index.yaml = doc\index.yaml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F900DCE0-EF45-4629-AA24-949E65BAB714}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.SpecFirst.Web", "src\Lab.SpecFirst.Web\Lab.SpecFirst.Web.csproj", "{70299524-F9F4-41DF-80EF-D1CE03C2965A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.SpecFirst.Adapter", "src\Lab.SpecFirst.Adapter\Lab.SpecFirst.Adapter.csproj", "{F5AAACC5-781F-41E6-8CD5-39389A00942C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "components", "components", "{4D3DDFBF-E6C5-4E38-8FC2-AE1C10450670}" + ProjectSection(SolutionItems) = preProject + doc\components\parameters.yaml = doc\components\parameters.yaml + doc\components\schemas.yaml = doc\components\schemas.yaml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "merge", "merge", "{F28E0DEE-5774-42CF-ABE2-2654AF751215}" + ProjectSection(SolutionItems) = preProject + doc\merge\index.yaml = doc\merge\index.yaml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {70299524-F9F4-41DF-80EF-D1CE03C2965A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70299524-F9F4-41DF-80EF-D1CE03C2965A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70299524-F9F4-41DF-80EF-D1CE03C2965A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70299524-F9F4-41DF-80EF-D1CE03C2965A}.Release|Any CPU.Build.0 = Release|Any CPU + {F5AAACC5-781F-41E6-8CD5-39389A00942C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5AAACC5-781F-41E6-8CD5-39389A00942C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5AAACC5-781F-41E6-8CD5-39389A00942C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5AAACC5-781F-41E6-8CD5-39389A00942C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {70299524-F9F4-41DF-80EF-D1CE03C2965A} = {F900DCE0-EF45-4629-AA24-949E65BAB714} + {F5AAACC5-781F-41E6-8CD5-39389A00942C} = {F900DCE0-EF45-4629-AA24-949E65BAB714} + {4D3DDFBF-E6C5-4E38-8FC2-AE1C10450670} = {7C405A84-132F-43F2-9F97-388CC40BED1D} + {F28E0DEE-5774-42CF-ABE2-2654AF751215} = {7C405A84-132F-43F2-9F97-388CC40BED1D} + EndGlobalSection +EndGlobal diff --git a/WebAPI/Swagger/Lab.SpecFirst2/Taskfile.yml b/WebAPI/Swagger/Lab.SpecFirst2/Taskfile.yml new file mode 100644 index 00000000..ae1af0b1 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/Taskfile.yml @@ -0,0 +1,29 @@ +version: "3" + +dotenv: [ "secrets/secrets.env" ] + +tasks: + spec-codegen: + desc: 產生 Client / Server Code + cmds: + - task: spec-merge-file + - task: spec-codegen-client + - task: spec-codegen-server + + spec-codegen-client: + desc: 產生 Client Code + cmds: + - nswag openapi2csclient /input:doc/merge/index.yaml /classname:LabSpecClient /namespace:Lab.SpecFirst.Adapter /output:src/Lab.SpecFirst.Adapter/AutoGenerated/LabSpecClient.cs /jsonLibrary:SystemTextJson /generateClientInterfaces:true /exposeJsonSerializerSettings:false /useBaseUrl:false + + spec-codegen-server: + desc: 產生 Server Code + cmds: + - nswag openapi2cscontroller /input:doc/merge/index.yaml /classname:SpecFirstContract /namespace:Lab.SpecFirst.Web.Controllers /output:src/Lab.SpecFirst.Web/AutoGenerated/Controller.cs /jsonLibrary:SystemTextJson + + spec-merge-file: + desc: 合併 Swagger File + cmds: +# - speccy resolve ./doc/index.yaml -o ./doc/merge/index.yaml +# - swagger-cli bundle ./doc/index.yaml --outfile ./doc/merge/index.yaml --type yaml + - openapi-merger -i ./doc/index.yaml -o ./doc/merge/index.yaml + \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst2/doc/components/index.yaml b/WebAPI/Swagger/Lab.SpecFirst2/doc/components/index.yaml new file mode 100644 index 00000000..21c814fc --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/doc/components/index.yaml @@ -0,0 +1,9 @@ +parameters: + petId: + name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + example: 1 \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst2/doc/components/parameters.yaml b/WebAPI/Swagger/Lab.SpecFirst2/doc/components/parameters.yaml new file mode 100644 index 00000000..b8dcfa9a --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/doc/components/parameters.yaml @@ -0,0 +1,8 @@ +petId: + name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + example: 1 \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst2/doc/components/schemas.yaml b/WebAPI/Swagger/Lab.SpecFirst2/doc/components/schemas.yaml new file mode 100644 index 00000000..bf11a600 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/doc/components/schemas.yaml @@ -0,0 +1,30 @@ +Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + +Pets: + type: array + items: + $ref: "#/Pet" + +Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst2/doc/index.yaml b/WebAPI/Swagger/Lab.SpecFirst2/doc/index.yaml new file mode 100644 index 00000000..04dd77db --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/doc/index.yaml @@ -0,0 +1,78 @@ +openapi: "3.0.3" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://localhost:7087/api/ +# - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "components/schemas.yaml#/Pets" + + default: + description: unexpected error + content: + application/json: + schema: + $ref: 'components/schemas.yaml#/Error' + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "components/schemas.yaml#/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - $ref: "components/parameters.yaml#/petId" + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "components/schemas.yaml#/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "components/schemas.yaml#/Error" \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst2/doc/merge/index.yaml b/WebAPI/Swagger/Lab.SpecFirst2/doc/merge/index.yaml new file mode 100644 index 00000000..8b5b6d4d --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/doc/merge/index.yaml @@ -0,0 +1,115 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: 'http://localhost:7087/api/' +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pets' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '/pets/{petId}': + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - $ref: '#/components/parameters/petId' + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + parameters: + petId: + name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + example: 1 + schemas: + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Adapter/AutoGenerated/LabSpecClient.cs b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Adapter/AutoGenerated/LabSpecClient.cs new file mode 100644 index 00000000..1aecf161 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Adapter/AutoGenerated/LabSpecClient.cs @@ -0,0 +1,531 @@ +//---------------------- +// +// Generated using the NSwag toolchain v13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" + +namespace Lab.SpecFirst.Adapter +{ + using System = global::System; + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + public partial interface ILabSpecClient + { + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + /// A server side error occurred. + System.Threading.Tasks.Task> ListPetsAsync(int? limit); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + /// A server side error occurred. + System.Threading.Tasks.Task> ListPetsAsync(int? limit, System.Threading.CancellationToken cancellationToken); + + /// Create a pet + /// Null response + /// A server side error occurred. + System.Threading.Tasks.Task CreatePetsAsync(); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Create a pet + /// Null response + /// A server side error occurred. + System.Threading.Tasks.Task CreatePetsAsync(System.Threading.CancellationToken cancellationToken); + + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + /// A server side error occurred. + System.Threading.Tasks.Task ShowPetByIdAsync(string petId); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + /// A server side error occurred. + System.Threading.Tasks.Task ShowPetByIdAsync(string petId, System.Threading.CancellationToken cancellationToken); + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class LabSpecClient : ILabSpecClient + { + private System.Net.Http.HttpClient _httpClient; + private System.Lazy _settings; + + public LabSpecClient(System.Net.Http.HttpClient httpClient) + { + _httpClient = httpClient; + _settings = new System.Lazy(CreateSerializerSettings); + } + + private System.Text.Json.JsonSerializerOptions CreateSerializerSettings() + { + var settings = new System.Text.Json.JsonSerializerOptions(); + UpdateJsonSerializerSettings(settings); + return settings; + } + + protected System.Text.Json.JsonSerializerOptions JsonSerializerSettings { get { return _settings.Value; } } + + partial void UpdateJsonSerializerSettings(System.Text.Json.JsonSerializerOptions settings); + + + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url); + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder); + partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response); + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + /// A server side error occurred. + public System.Threading.Tasks.Task> ListPetsAsync(int? limit) + { + return ListPetsAsync(limit, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + /// A server side error occurred. + public async System.Threading.Tasks.Task> ListPetsAsync(int? limit, System.Threading.CancellationToken cancellationToken) + { + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append("pets?"); + if (limit != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("limit") + "=").Append(System.Uri.EscapeDataString(ConvertToString(limit, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + urlBuilder_.Length--; + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("unexpected error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// Create a pet + /// Null response + /// A server side error occurred. + public System.Threading.Tasks.Task CreatePetsAsync() + { + return CreatePetsAsync(System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Create a pet + /// Null response + /// A server side error occurred. + public async System.Threading.Tasks.Task CreatePetsAsync(System.Threading.CancellationToken cancellationToken) + { + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append("pets"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 201) + { + return; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("unexpected error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + /// A server side error occurred. + public System.Threading.Tasks.Task ShowPetByIdAsync(string petId) + { + return ShowPetByIdAsync(petId, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + /// A server side error occurred. + public async System.Threading.Tasks.Task ShowPetByIdAsync(string petId, System.Threading.CancellationToken cancellationToken) + { + if (petId == null) + throw new System.ArgumentNullException("petId"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append("pets/{petId}"); + urlBuilder_.Replace("{petId}", System.Uri.EscapeDataString(ConvertToString(petId, System.Globalization.CultureInfo.InvariantCulture))); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("unexpected error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + protected struct ObjectResponseResult + { + public ObjectResponseResult(T responseObject, string responseText) + { + this.Object = responseObject; + this.Text = responseText; + } + + public T Object { get; } + + public string Text { get; } + } + + public bool ReadResponseAsString { get; set; } + + protected virtual async System.Threading.Tasks.Task> ReadObjectResponseAsync(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Threading.CancellationToken cancellationToken) + { + if (response == null || response.Content == null) + { + return new ObjectResponseResult(default(T), string.Empty); + } + + if (ReadResponseAsString) + { + var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + try + { + var typedBody = System.Text.Json.JsonSerializer.Deserialize(responseText, JsonSerializerSettings); + return new ObjectResponseResult(typedBody, responseText); + } + catch (System.Text.Json.JsonException exception) + { + var message = "Could not deserialize the response body string as " + typeof(T).FullName + "."; + throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception); + } + } + else + { + try + { + using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + var typedBody = await System.Text.Json.JsonSerializer.DeserializeAsync(responseStream, JsonSerializerSettings, cancellationToken).ConfigureAwait(false); + return new ObjectResponseResult(typedBody, string.Empty); + } + } + catch (System.Text.Json.JsonException exception) + { + var message = "Could not deserialize the response body stream as " + typeof(T).FullName + "."; + throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception); + } + } + } + + private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo) + { + if (value == null) + { + return ""; + } + + if (value is System.Enum) + { + var name = System.Enum.GetName(value.GetType(), value); + if (name != null) + { + var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name); + if (field != null) + { + var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute)) + as System.Runtime.Serialization.EnumMemberAttribute; + if (attribute != null) + { + return attribute.Value != null ? attribute.Value : name; + } + } + + var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo)); + return converted == null ? string.Empty : converted; + } + } + else if (value is bool) + { + return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant(); + } + else if (value is byte[]) + { + return System.Convert.ToBase64String((byte[]) value); + } + else if (value.GetType().IsArray) + { + var array = System.Linq.Enumerable.OfType((System.Array) value); + return string.Join(",", System.Linq.Enumerable.Select(array, o => ConvertToString(o, cultureInfo))); + } + + var result = System.Convert.ToString(value, cultureInfo); + return result == null ? "" : result; + } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Error + { + [System.Text.Json.Serialization.JsonPropertyName("code")] + public int Code { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("message")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Message { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties = new System.Collections.Generic.Dictionary(); + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties; } + set { _additionalProperties = value; } + } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Pet + { + [System.Text.Json.Serialization.JsonPropertyName("id")] + public long Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("name")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Name { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("tag")] + public string Tag { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties = new System.Collections.Generic.Dictionary(); + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties; } + set { _additionalProperties = value; } + } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Pets : System.Collections.ObjectModel.Collection + { + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class ApiException : System.Exception + { + public int StatusCode { get; private set; } + + public string Response { get; private set; } + + public System.Collections.Generic.IReadOnlyDictionary> Headers { get; private set; } + + public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Exception innerException) + : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException) + { + StatusCode = statusCode; + Response = response; + Headers = headers; + } + + public override string ToString() + { + return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString()); + } + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class ApiException : ApiException + { + public TResult Result { get; private set; } + + public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary> headers, TResult result, System.Exception innerException) + : base(message, statusCode, response, headers, innerException) + { + Result = result; + } + } + +} + +#pragma warning restore 1591 +#pragma warning restore 1573 +#pragma warning restore 472 +#pragma warning restore 114 +#pragma warning restore 108 +#pragma warning restore 3016 \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Adapter/Lab.SpecFirst.Adapter.csproj b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Adapter/Lab.SpecFirst.Adapter.csproj new file mode 100644 index 00000000..eb2460e9 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Adapter/Lab.SpecFirst.Adapter.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/.dockerignore b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/AutoGenerated/Controller.cs b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/AutoGenerated/Controller.cs new file mode 100644 index 00000000..ddb19bf6 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/AutoGenerated/Controller.cs @@ -0,0 +1,137 @@ +//---------------------- +// +// Generated using the NSwag toolchain v13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" + +namespace Lab.SpecFirst.Web.Controllers +{ + using System = global::System; + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + public interface ISpecFirstContractController + { + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + System.Threading.Tasks.Task> ListPetsAsync(int? limit); + + /// Create a pet + /// Null response + System.Threading.Tasks.Task CreatePetsAsync(); + + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + System.Threading.Tasks.Task ShowPetByIdAsync(string petId); + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0))")] + [Microsoft.AspNetCore.Mvc.Route("api/")] + public partial class SpecFirstContractController : Microsoft.AspNetCore.Mvc.ControllerBase + { + private ISpecFirstContractController _implementation; + + public SpecFirstContractController(ISpecFirstContractController implementation) + { + _implementation = implementation; + } + + /// List all pets + /// How many items to return at one time (max 100) + /// A paged array of pets + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("pets")] + public System.Threading.Tasks.Task> ListPets([Microsoft.AspNetCore.Mvc.FromQuery] int? limit) + { + return _implementation.ListPetsAsync(limit); + } + + /// Create a pet + /// Null response + [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("pets")] + public System.Threading.Tasks.Task CreatePets() + { + return _implementation.CreatePetsAsync(); + } + + /// Info for a specific pet + /// The id of the pet to retrieve + /// Expected response to a valid request + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("pets/{petId}")] + public System.Threading.Tasks.Task ShowPetById(string petId) + { + return _implementation.ShowPetByIdAsync(petId); + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Error + { + [System.Text.Json.Serialization.JsonPropertyName("code")] + public int Code { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("message")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Message { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties = new System.Collections.Generic.Dictionary(); + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties; } + set { _additionalProperties = value; } + } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Pet + { + [System.Text.Json.Serialization.JsonPropertyName("id")] + public long Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("name")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Name { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("tag")] + public string Tag { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties = new System.Collections.Generic.Dictionary(); + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties; } + set { _additionalProperties = value; } + } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v11.0.0.0)")] + public partial class Pets : System.Collections.ObjectModel.Collection + { + + } + +} + +#pragma warning restore 1591 +#pragma warning restore 1573 +#pragma warning restore 472 +#pragma warning restore 114 +#pragma warning restore 108 +#pragma warning restore 3016 \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Controllers/SpecFirstController.cs b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Controllers/SpecFirstController.cs new file mode 100644 index 00000000..eed7b89a --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Controllers/SpecFirstController.cs @@ -0,0 +1,38 @@ +namespace Lab.SpecFirst.Web.Controllers; + +class SpecFirstController : ISpecFirstContractController +{ + public async Task> ListPetsAsync(int? limit) + { + return new List() + { + new() + { + Id = 1, + Name = "yao", + Tag = "dog", + AdditionalProperties = new Dictionary() + { + } + }, + }; + } + + public async Task CreatePetsAsync() + { + + } + + public async Task ShowPetByIdAsync(string petId) + { + return new() + { + Id = 1, + Name = "yao", + Tag = "dog", + AdditionalProperties = new Dictionary() + { + } + }; + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Dockerfile b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Dockerfile new file mode 100644 index 00000000..e568de25 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["src/Lab.SpecFirst.Web/Lab.SpecFirst.Web.csproj", "Lab.SpecFirst.Web/"] +RUN dotnet restore "src/Lab.SpecFirst.Web/Lab.SpecFirst.Web.csproj" +COPY . . +WORKDIR "/src/Lab.SpecFirst.Web" +RUN dotnet build "Lab.SpecFirst.Web.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Lab.SpecFirst.Web.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Lab.SpecFirst.Web.dll"] diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Lab.SpecFirst.Web.csproj b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Lab.SpecFirst.Web.csproj new file mode 100644 index 00000000..0db9dec4 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Lab.SpecFirst.Web.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + Linux + + + + + + + diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Program.cs b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Program.cs new file mode 100644 index 00000000..8f8bd1ae --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Program.cs @@ -0,0 +1,29 @@ +using Lab.SpecFirst.Web.Controllers; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddScoped(); + +// Add services to the container. +builder.Services.AddControllers(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Properties/launchSettings.json b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Properties/launchSettings.json new file mode 100644 index 00000000..466bd7be --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:9860", + "sslPort": 44313 + } + }, + "profiles": { + "Lab.SpecFirst.Web": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7041;http://localhost:5041", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/WeatherForecast.cs b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/WeatherForecast.cs new file mode 100644 index 00000000..bacb51a3 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Lab.SpecFirst.Web; + +public class WeatherForecast +{ + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/appsettings.Development.json b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/appsettings.json b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/WebAPI/Swagger/Lab.SpecFirst2/src/Lab.SpecFirst.Web/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.SwaggerDoc.MultiVersion.sln b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.SwaggerDoc.MultiVersion.sln new file mode 100644 index 00000000..db633377 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.SwaggerDoc.MultiVersion.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Swashbuckle.AspNetCore6", "Lab.Swashbuckle.AspNetCore6\Lab.Swashbuckle.AspNetCore6.csproj", "{2DA608D5-BC95-4DF6-AB7A-0A22085E9257}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiVersioningSample", "..\..\..\..\lab\blogsamples\ApiVersioningSample\ApiVersioningSample.csproj", "{D3DAD3B2-CA83-40E0-90B7-BF005C7C7210}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2DA608D5-BC95-4DF6-AB7A-0A22085E9257}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DA608D5-BC95-4DF6-AB7A-0A22085E9257}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DA608D5-BC95-4DF6-AB7A-0A22085E9257}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DA608D5-BC95-4DF6-AB7A-0A22085E9257}.Release|Any CPU.Build.0 = Release|Any CPU + {D3DAD3B2-CA83-40E0-90B7-BF005C7C7210}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3DAD3B2-CA83-40E0-90B7-BF005C7C7210}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3DAD3B2-CA83-40E0-90B7-BF005C7C7210}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3DAD3B2-CA83-40E0-90B7-BF005C7C7210}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/ConfigureApiVersionSwaggerGenOptions.cs b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/ConfigureApiVersionSwaggerGenOptions.cs new file mode 100644 index 00000000..a401bd5f --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/ConfigureApiVersionSwaggerGenOptions.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Lab.Swashbuckle.AspNetCore6; + +public class ConfigureApiVersionSwaggerGenOptions : IConfigureOptions +{ + private readonly IApiVersionDescriptionProvider _provider; + + public ConfigureApiVersionSwaggerGenOptions(IApiVersionDescriptionProvider provider) + { + _provider = provider; + } + + public void Configure(SwaggerGenOptions options) + { + foreach (var description in _provider.ApiVersionDescriptions) + { + options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); + } + } + + private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) + { + //產生 API 資訊 + var info = new OpenApiInfo + { + Version = description.ApiVersion.ToString(), + Title = "Employee API", + Description = + @"

Sample API with versioning including Swagger.

Partly taken from this repository.

", + TermsOfService = new Uri("https://example.com/terms"), + Contact = new OpenApiContact + { + Name = "Example Contact", + Url = new Uri("https://example.com/contact") + }, + License = new OpenApiLicense + { + Name = "Example License", + Url = new Uri("https://example.com/license") + } + }; + + if (description.IsDeprecated) + { + info.Description += + @"

VERSION IS DEPRECATED

"; + } + + return info; + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Controllers/Employee/v1_0/DemoController.cs b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Controllers/Employee/v1_0/DemoController.cs new file mode 100644 index 00000000..8c32b471 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Controllers/Employee/v1_0/DemoController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.Swashbuckle.AspNetCore6.Controllers.Employee.v1_0; + +[ApiVersion("1.0", Deprecated = true)] +[ApiController] +// [Route("api/[controller]")] +[Route("api/v{version:apiVersion}/[controller]")] +public class DemoController : ControllerBase +{ + [HttpGet] + public IActionResult Get() + { + return this.Ok(new + { + Version = 1.0, + Name = "1.0" + }); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Controllers/Employee/v1_1/DemoController.cs b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Controllers/Employee/v1_1/DemoController.cs new file mode 100644 index 00000000..7c29fdeb --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Controllers/Employee/v1_1/DemoController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.Swashbuckle.AspNetCore6.Controllers.Employee.v1_1; + +[ApiVersion("1.1")] +[ApiController] +// [Route("api/[controller]")] +[Route("api/v{version:apiVersion}/[controller]")] +public class DemoController : ControllerBase +{ + [HttpGet] + public IActionResult Get() + { + return this.Ok(new + { + Version = 1.1, + Name = "1.1" + }); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Lab.Swashbuckle.AspNetCore6.csproj b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Lab.Swashbuckle.AspNetCore6.csproj new file mode 100644 index 00000000..c2f22baf --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Lab.Swashbuckle.AspNetCore6.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + bin\ + bin\Lab.Swashbuckle.AspNetCore6.xml + + + + + + + + + + + + + + + diff --git a/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Program.cs b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Program.cs new file mode 100644 index 00000000..8158e877 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Program.cs @@ -0,0 +1,90 @@ +using System.Reflection; +using System.Xml.XPath; +using Lab.Swashbuckle.AspNetCore6; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.Filters; +using Swashbuckle.AspNetCore.SwaggerGen; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.ExampleFilters(); + + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); +}); +builder.Services.AddSwaggerExamplesFromAssemblies(Assembly.GetEntryAssembly()); +builder.Services.AddApiVersioning(option => +{ + //返回響應標頭中支援的版本資訊 + option.ReportApiVersions = true; + + //未提供版本請請時,使用預設版號 + option.AssumeDefaultVersionWhenUnspecified = false; + + //預設api版本號,支援時間或數字版本號 + option.DefaultApiVersion = new ApiVersion(1, 0); + + //支援MediaType、Header、QueryString 設定版本號 + option.ApiVersionReader = ApiVersionReader.Combine( + new MediaTypeApiVersionReader("api-version"), + new HeaderApiVersionReader("api-version"), + new QueryStringApiVersionReader("api-version"), + new UrlSegmentApiVersionReader()); +}); + +builder.Services.AddVersionedApiExplorer(options => +{ + + // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service + // note: the specified format code will format the version as "'v'major[.minor][-status]" + //options.GroupNameFormat = "'v'VVV"; + + // note: this option is only necessary when versioning by url segment. the SubstitutionFormat + // can also be used to control the format of the API version in route templates + //options.SubstituteApiVersionInUrl = true; +}); +builder.Services.AddSingleton, ConfigureApiVersionSwaggerGenOptions>(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + + app.UseSwaggerUI( + options => + { + var provider = app.Services.GetService(); + + // build a swagger endpoint for each discovered API version + foreach (var description in provider.ApiVersionDescriptions) + { + var url = $"/swagger/{description.GroupName}/swagger.json"; + options.SwaggerEndpoint(url, + description.GroupName.ToUpperInvariant()); + } + }); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.UseApiVersioning(); + +app.Run(); \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Properties/launchSettings.json b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Properties/launchSettings.json new file mode 100644 index 00000000..a270a625 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51786", + "sslPort": 44377 + } + }, + "profiles": { + "Lab.Swashbuckle.AspNetCore6": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7236;http://localhost:5236", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/appsettings.Development.json b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/appsettings.json b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc.MultiVersion/Lab.Swashbuckle.AspNetCore6/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/WebAPI/Swagger/Lab.SwaggerDoc/Lab.SwaggerDoc.sln b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.SwaggerDoc.sln new file mode 100644 index 00000000..59eaf419 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.SwaggerDoc.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Swashbuckle.AspNetCore6", "Lab.Swashbuckle.AspNetCore6\Lab.Swashbuckle.AspNetCore6.csproj", "{2DA608D5-BC95-4DF6-AB7A-0A22085E9257}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2DA608D5-BC95-4DF6-AB7A-0A22085E9257}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DA608D5-BC95-4DF6-AB7A-0A22085E9257}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DA608D5-BC95-4DF6-AB7A-0A22085E9257}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DA608D5-BC95-4DF6-AB7A-0A22085E9257}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Controllers/EmployeeController.cs b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Controllers/EmployeeController.cs new file mode 100644 index 00000000..bee2d16e --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Controllers/EmployeeController.cs @@ -0,0 +1,70 @@ +using Lab.Swashbuckle.AspNetCore6.Examples; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using Swashbuckle.AspNetCore.Filters; + +namespace Lab.Swashbuckle.AspNetCore6.Controllers; + +[ApiController] +[Route("[controller]")] +public class EmployeeController : ControllerBase +{ + private static readonly string[] Summaries = + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public EmployeeController(ILogger logger) + { + this._logger = logger; + } + + /// + /// 取得會員 + /// + /// + /// + /// Sample request: + /// + /// POST /Todo + /// { + /// "id": 1, + /// "name": "Item #1", + /// "isComplete": true + /// } + /// + + // [HttpGet(Name = "GetEmployee")] + [HttpGet] + [Produces("application/json")] + // [ProducesResponseType(typeof(EmployeeResponse), StatusCodes.Status200OK)] + [SwaggerResponse(200, "查詢結果", typeof(EmployeeResponse))] + [SwaggerRequestExample(typeof(QueryEmployeeRequest), typeof(QueryEmployeeRequestExample))] + [SwaggerResponseExample(200, typeof(EmployeeResponseExample))] + public async Task Get(QueryEmployeeRequest request) + { + if (this.ModelState.IsValid == false) + { + return this.BadRequest(); + } + + return this.Ok(new List + { + new() + { + Id = Guid.NewGuid(), + Name = "yao", + Age = 20 + }, + new() + { + Id = Guid.NewGuid(), + Name = "小章", + Age = 18, + Remark = "說明" + } + }); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/EmployeeResponse.cs b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/EmployeeResponse.cs new file mode 100644 index 00000000..99f842b9 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/EmployeeResponse.cs @@ -0,0 +1,24 @@ +namespace Lab.Swashbuckle.AspNetCore6; + +public class EmployeeResponse +{ + /// + /// 編號 + /// + public Guid Id { get; set; } + + /// + /// 姓名 + /// + public string Name { get; set; } + + /// + /// 年齡 + /// + public int Age { get; set; } + + /// + /// 註解 + /// + public string Remark { get; set; } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Examples/EmployeeResponseExample.cs b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Examples/EmployeeResponseExample.cs new file mode 100644 index 00000000..f0e56706 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Examples/EmployeeResponseExample.cs @@ -0,0 +1,17 @@ +using Swashbuckle.AspNetCore.Filters; + +namespace Lab.Swashbuckle.AspNetCore6.Examples; + +public class EmployeeResponseExample : IExamplesProvider +{ + public EmployeeResponse GetExamples() + { + return new EmployeeResponse + { + Id = Guid.Parse("00000000-0000-0000-0000-000000000001"), + Name = "小章", + Age = 18, + Remark = "說明" + }; + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Examples/QueryEmployeeRequestExample.cs b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Examples/QueryEmployeeRequestExample.cs new file mode 100644 index 00000000..44932cf9 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Examples/QueryEmployeeRequestExample.cs @@ -0,0 +1,16 @@ +using Swashbuckle.AspNetCore.Filters; + +namespace Lab.Swashbuckle.AspNetCore6.Examples; + +public class QueryEmployeeRequestExample : IExamplesProvider +{ + public QueryEmployeeRequest GetExamples() + { + return new QueryEmployeeRequest + { + Name = "小章", + Age = 18, + // State = (State)1 + }; + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Lab.Swashbuckle.AspNetCore6.csproj b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Lab.Swashbuckle.AspNetCore6.csproj new file mode 100644 index 00000000..bc9b1642 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Lab.Swashbuckle.AspNetCore6.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + bin\ + bin\Lab.Swashbuckle.AspNetCore6.xml + + + + + + + + + + diff --git a/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Program.cs b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Program.cs new file mode 100644 index 00000000..61e705bd --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Program.cs @@ -0,0 +1,97 @@ +using System.Reflection; +using System.Xml.XPath; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.Filters; +using Swashbuckle.AspNetCore.SwaggerGen; + +void IncludeXmlComments(Assembly assembly, SwaggerGenOptions swaggerGenOptions) +{ + var directory = AppDomain.CurrentDomain.BaseDirectory; + if (assembly != null) + { + foreach (var name in assembly.GetManifestResourceNames() + .Where(x => x.ToUpper() + .EndsWith(".XML")) + ) + { + try + { + var xPath = new XPathDocument(assembly.GetManifestResourceStream(name)); + swaggerGenOptions.IncludeXmlComments((Func)(() => xPath)); + } + catch + { + } + } + } + + if (string.IsNullOrEmpty(directory)) + { + return; + } + + foreach (var file in Directory.GetFiles(directory, "*.XML", SearchOption.AllDirectories)) + { + swaggerGenOptions.IncludeXmlComments(file); + } +} + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "Employee API", + Description = "An ASP.NET Core Web API for managing employees", + TermsOfService = new Uri("https://example.com/terms"), + Contact = new OpenApiContact + { + Name = "Example Contact", + Url = new Uri("https://example.com/contact") + }, + License = new OpenApiLicense + { + Name = "Example License", + Url = new Uri("https://example.com/license") + } + }); + options.SwaggerDoc("v2", new OpenApiInfo + { + Version = "v2", + Title = "Employee API" + }); + options.ExampleFilters(); + + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); +}); +builder.Services.AddSwaggerExamplesFromAssemblies(Assembly.GetEntryAssembly()); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); + options.SwaggerEndpoint("/swagger/v2/swagger.json", "v2"); + }); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Properties/launchSettings.json b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Properties/launchSettings.json new file mode 100644 index 00000000..a270a625 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51786", + "sslPort": 44377 + } + }, + "profiles": { + "Lab.Swashbuckle.AspNetCore6": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7236;http://localhost:5236", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/QueryEmployeeRequest.cs b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/QueryEmployeeRequest.cs new file mode 100644 index 00000000..48f021d9 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/QueryEmployeeRequest.cs @@ -0,0 +1,50 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Lab.Swashbuckle.AspNetCore6; + +public class QueryEmployeeRequest +{ + /// + /// 姓名 + /// + /// 小章 + [Required] + public string Name { get; set; } + + /// + /// 年齡 + /// + /// 18 + public int Age { get; set; } + + /// + /// 狀態 + /// + /// 1 + public State State { get; set; } +} + +[JsonConverter(typeof(JsonStringEnumMemberConverter))] // This custom converter was placed in a system namespace. +public enum State +{ + [EnumMember(Value = "UNKNOWN_DEFINITION_000")] + + None = 0, + + /// + /// Approved + /// + /// Approved + // [Description("Approved")] + [EnumMember(Value = "Approved")] + Approved = 1, + + /// + /// Rejected + /// + [EnumMember(Value = "Rejected")] + Rejected = 2 +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/appsettings.Development.json b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/appsettings.json b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/WebAPI/Swagger/Lab.SwaggerDoc/Lab.Swashbuckle.AspNetCore6/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/WebAPI/Swagger/Mock Server/Prism/doc/1/index.yaml b/WebAPI/Swagger/Mock Server/Prism/doc/1/index.yaml new file mode 100644 index 00000000..acdeb1fd --- /dev/null +++ b/WebAPI/Swagger/Mock Server/Prism/doc/1/index.yaml @@ -0,0 +1,111 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string \ No newline at end of file diff --git a/WebAPI/Swagger/Mock Server/Prism/doc/2/index.yaml b/WebAPI/Swagger/Mock Server/Prism/doc/2/index.yaml new file mode 100644 index 00000000..79724a80 --- /dev/null +++ b/WebAPI/Swagger/Mock Server/Prism/doc/2/index.yaml @@ -0,0 +1,169 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + example: + - id: 10 + name: "doggie" + tag: "dog" + + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + examples: + 1: + value: + id: 1 + name: "doggie" + tag: "dog" + 12: + value: + id: 12 + name: "dora" + tag: "cat" + '404': + description: Pet not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + message: "Pet not found" + status: 404 + code: 404 + + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/validate: + get: + summary: Your GET endpoint + tags: [] + parameters: + - schema: + type: string + pattern: '^[A-Z]{2} [1-9]{4}$' + minLength: 0 + in: query + name: id + required: true + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + examples: + 1: + value: + id: 1 + name: "doggie" + tag: "dog" + 12: + value: + id: 12 + name: "dora" + tag: "cat" + +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string \ No newline at end of file diff --git a/WebAPI/Swagger/Mock Server/Prism/src/Lab.MockServer.sln b/WebAPI/Swagger/Mock Server/Prism/src/Lab.MockServer.sln new file mode 100644 index 00000000..3cf1d2a9 --- /dev/null +++ b/WebAPI/Swagger/Mock Server/Prism/src/Lab.MockServer.sln @@ -0,0 +1,26 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "doc", "doc", "{3B9B5D72-1C50-43D1-8E33-FBE93C4D0FF8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1", "1", "{DA17785E-5C85-4A31-AFA6-B5BF799B3DDD}" + ProjectSection(SolutionItems) = preProject + ..\doc\1\index.yaml = ..\doc\1\index.yaml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2", "2", "{F7194878-92F6-4FE5-9841-DE99711949F6}" + ProjectSection(SolutionItems) = preProject + ..\doc\2\index.yaml = ..\doc\2\index.yaml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{A84DAC1D-5160-47E1-916B-892AB78EA2E8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DA17785E-5C85-4A31-AFA6-B5BF799B3DDD} = {3B9B5D72-1C50-43D1-8E33-FBE93C4D0FF8} + {F7194878-92F6-4FE5-9841-DE99711949F6} = {3B9B5D72-1C50-43D1-8E33-FBE93C4D0FF8} + EndGlobalSection +EndGlobal