diff --git a/AWS/Lab.AwsDDB/Lab.AwsDDB.sln b/AWS/Lab.AwsDDB/Lab.AwsDDB.sln new file mode 100644 index 00000000..8c7ba129 --- /dev/null +++ b/AWS/Lab.AwsDDB/Lab.AwsDDB.sln @@ -0,0 +1,21 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.AwsDDB.Test", "test\Lab.AwsDDB.Test\Lab.AwsDDB.Test.csproj", "{5B2F9C1D-597C-4890-9806-42B42D42F15C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{23EBD672-F330-4AEB-9F14-24C0E562F670}" + 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 + {5B2F9C1D-597C-4890-9806-42B42D42F15C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B2F9C1D-597C-4890-9806-42B42D42F15C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B2F9C1D-597C-4890-9806-42B42D42F15C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B2F9C1D-597C-4890-9806-42B42D42F15C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/AWS/Lab.AwsDDB/docker-compose.yml b/AWS/Lab.AwsDDB/docker-compose.yml new file mode 100644 index 00000000..4dd4623e --- /dev/null +++ b/AWS/Lab.AwsDDB/docker-compose.yml @@ -0,0 +1,17 @@ +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 \ No newline at end of file diff --git a/AWS/Lab.AwsDDB/test/Lab.AwsDDB.Test/Lab.AwsDDB.Test.csproj b/AWS/Lab.AwsDDB/test/Lab.AwsDDB.Test/Lab.AwsDDB.Test.csproj new file mode 100644 index 00000000..24fa062f --- /dev/null +++ b/AWS/Lab.AwsDDB/test/Lab.AwsDDB.Test/Lab.AwsDDB.Test.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + diff --git a/AWS/Lab.AwsDDB/test/Lab.AwsDDB.Test/UnitTest1.cs b/AWS/Lab.AwsDDB/test/Lab.AwsDDB.Test/UnitTest1.cs new file mode 100644 index 00000000..cddd0eb9 --- /dev/null +++ b/AWS/Lab.AwsDDB/test/Lab.AwsDDB.Test/UnitTest1.cs @@ -0,0 +1,524 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.DocumentModel; +using Amazon.DynamoDBv2.Model; + +namespace Lab.AwsDDB.Test; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void TestMethod1() + { + var clientConfig = new AmazonDynamoDBConfig(); + clientConfig.ServiceURL = "http://localhost:8000"; + var client = new AmazonDynamoDBClient(clientConfig); + // CreateTableProductCatalog(client); + LoadSampleProducts(client); + } + + // static void Main(string[] args) + // { + // try + // { + // //DeleteAllTables(client); + // DeleteTable("ProductCatalog"); + // DeleteTable("Forum"); + // DeleteTable("Thread"); + // DeleteTable("Reply"); + // + // // Create tables (using the AWS SDK for .NET low-level API). + // CreateTableProductCatalog(); + // CreateTableForum(); + // CreateTableThread(); // ForumTitle, Subject */ + // CreateTableReply(); + // + // // Load data (using the .NET SDK document API) + // LoadSampleProducts(); + // LoadSampleForums(); + // LoadSampleThreads(); + // LoadSampleReplies(); + // Console.WriteLine("Sample complete!"); + // Console.WriteLine("Press ENTER to continue"); + // Console.ReadLine(); + // } + // catch (AmazonServiceException e) { Console.WriteLine(e.Message); } + // catch (Exception e) { Console.WriteLine(e.Message); } + // } + + private static async Task DeleteTable(AmazonDynamoDBClient client, string tableName) + { + try + { + var deleteTableResponse = await client.DeleteTableAsync(new DeleteTableRequest() + { + TableName = tableName + }); + WaitTillTableDeleted(client, tableName, deleteTableResponse); + } + catch (ResourceNotFoundException) + { + // There is no such table. + } + } + + private static async Task CreateTableProductCatalog(AmazonDynamoDBClient client) + { + string tableName = "ProductCatalog"; + + var response = await client.CreateTableAsync(new CreateTableRequest + { + TableName = tableName, + AttributeDefinitions = new List() + { + new() + { + AttributeName = "Id", + AttributeType = "N" + } + }, + KeySchema = new List() + { + new() + { + AttributeName = "Id", + KeyType = "HASH" + } + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 10, + WriteCapacityUnits = 5 + } + }); + + WaitTillTableCreated(client, tableName, response); + } + + private static async Task CreateTableForum(AmazonDynamoDBClient client) + { + string tableName = "Forum"; + + var response = await client.CreateTableAsync(new CreateTableRequest + { + TableName = tableName, + AttributeDefinitions = new List() + { + new AttributeDefinition + { + AttributeName = "Name", + AttributeType = "S" + } + }, + KeySchema = new List() + { + new KeySchemaElement + { + AttributeName = "Name", // forum Title + KeyType = "HASH" + } + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 10, + WriteCapacityUnits = 5 + } + }); + + WaitTillTableCreated(client, tableName, response); + } + + private static async Task CreateTableThread(AmazonDynamoDBClient client) + { + string tableName = "Thread"; + + var response = await client.CreateTableAsync(new CreateTableRequest + { + TableName = tableName, + AttributeDefinitions = new List() + { + new AttributeDefinition + { + AttributeName = "ForumName", // Hash attribute + AttributeType = "S" + }, + new AttributeDefinition + { + AttributeName = "Subject", + AttributeType = "S" + } + }, + KeySchema = new List() + { + new KeySchemaElement + { + AttributeName = "ForumName", // Hash attribute + KeyType = "HASH" + }, + new KeySchemaElement + { + AttributeName = "Subject", // Range attribute + KeyType = "RANGE" + } + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 10, + WriteCapacityUnits = 5 + } + }); + + WaitTillTableCreated(client, tableName, response); + } + + private static async Task CreateTableReply(AmazonDynamoDBClient client) + { + string tableName = "Reply"; + var response = await client.CreateTableAsync(new CreateTableRequest + { + TableName = tableName, + AttributeDefinitions = new List() + { + new AttributeDefinition + { + AttributeName = "Id", + AttributeType = "S" + }, + new AttributeDefinition + { + AttributeName = "ReplyDateTime", + AttributeType = "S" + }, + new AttributeDefinition + { + AttributeName = "PostedBy", + AttributeType = "S" + } + }, + KeySchema = new List() + { + new KeySchemaElement() + { + AttributeName = "Id", + KeyType = "HASH" + }, + new KeySchemaElement() + { + AttributeName = "ReplyDateTime", + KeyType = "RANGE" + } + }, + LocalSecondaryIndexes = new List() + { + new LocalSecondaryIndex() + { + IndexName = "PostedBy_index", + + KeySchema = new List() + { + new KeySchemaElement() + { + AttributeName = "Id", KeyType = "HASH" + }, + new KeySchemaElement() + { + AttributeName = "PostedBy", KeyType = "RANGE" + } + }, + Projection = new Projection() + { + ProjectionType = ProjectionType.KEYS_ONLY + } + } + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 10, + WriteCapacityUnits = 5 + } + }); + + WaitTillTableCreated(client, tableName, response); + } + + private static async Task WaitTillTableCreated(AmazonDynamoDBClient client, + string tableName, + CreateTableResponse response) + { + var tableDescription = response.TableDescription; + + string status = tableDescription.TableStatus; + + Console.WriteLine(tableName + " - " + status); + + // Let us wait until table is created. Call DescribeTable. + while (status != "ACTIVE") + { + System.Threading.Thread.Sleep(5000); // Wait 5 seconds. + try + { + var res = await client.DescribeTableAsync(new DescribeTableRequest + { + TableName = tableName + }); + Console.WriteLine("Table name: {0}, status: {1}", res.Table.TableName, + res.Table.TableStatus); + status = res.Table.TableStatus; + } + + // Try-catch to handle potential eventual-consistency issue. + catch (ResourceNotFoundException) + { + } + } + } + + private static async Task WaitTillTableDeleted(AmazonDynamoDBClient client, string tableName, + DeleteTableResponse response) + { + var tableDescription = response.TableDescription; + + string status = tableDescription.TableStatus; + + Console.WriteLine(tableName + " - " + status); + + // Let us wait until table is created. Call DescribeTable + try + { + while (status == "DELETING") + { + System.Threading.Thread.Sleep(5000); // wait 5 seconds + + var res = await client.DescribeTableAsync(new DescribeTableRequest + { + TableName = tableName + }); + Console.WriteLine("Table name: {0}, status: {1}", res.Table.TableName, + res.Table.TableStatus); + status = res.Table.TableStatus; + } + } + catch (ResourceNotFoundException) + { + // Table deleted. + } + } + + private static async Task LoadSampleProducts(AmazonDynamoDBClient client) + { + Table productCatalogTable = Table.LoadTable(client, "ProductCatalog"); + + // ********** Add Books ********************* + var book1 = new Document(); + book1["Id"] = 101; + book1["Title"] = "Book 101 Title"; + book1["ISBN"] = "111-1111111111"; + book1["Authors"] = new List { "Author 1" }; + book1["Price"] = -2; // *** Intentional value. Later used to illustrate scan. + book1["Dimensions"] = "8.5 x 11.0 x 0.5"; + book1["PageCount"] = 500; + book1["InPublication"] = true; + book1["ProductCategory"] = "Book"; + await productCatalogTable.PutItemAsync(book1); + + var book2 = new Document(); + + book2["Id"] = 102; + book2["Title"] = "Book 102 Title"; + book2["ISBN"] = "222-2222222222"; + book2["Authors"] = new List { "Author 1", "Author 2" }; + ; + book2["Price"] = 20; + book2["Dimensions"] = "8.5 x 11.0 x 0.8"; + book2["PageCount"] = 600; + book2["InPublication"] = true; + book2["ProductCategory"] = "Book"; + await productCatalogTable.PutItemAsync(book2); + + var book3 = new Document(); + book3["Id"] = 103; + book3["Title"] = "Book 103 Title"; + book3["ISBN"] = "333-3333333333"; + book3["Authors"] = new List { "Author 1", "Author2", "Author 3" }; + ; + book3["Price"] = 2000; + book3["Dimensions"] = "8.5 x 11.0 x 1.5"; + book3["PageCount"] = 700; + book3["InPublication"] = false; + book3["ProductCategory"] = "Book"; + await productCatalogTable.PutItemAsync(book3); + + // ************ Add bikes. ******************* + var bicycle1 = new Document(); + bicycle1["Id"] = 201; + bicycle1["Title"] = "18-Bike 201"; // size, followed by some title. + bicycle1["Description"] = "201 description"; + bicycle1["BicycleType"] = "Road"; + bicycle1["Brand"] = "Brand-Company A"; // Trek, Specialized. + bicycle1["Price"] = 100; + bicycle1["Color"] = new List { "Red", "Black" }; + bicycle1["ProductCategory"] = "Bike"; + await productCatalogTable.PutItemAsync(bicycle1); + + var bicycle2 = new Document(); + bicycle2["Id"] = 202; + bicycle2["Title"] = "21-Bike 202Brand-Company A"; + bicycle2["Description"] = "202 description"; + bicycle2["BicycleType"] = "Road"; + bicycle2["Brand"] = ""; + bicycle2["Price"] = 200; + bicycle2["Color"] = new List { "Green", "Black" }; + bicycle2["ProductCategory"] = "Bicycle"; + await productCatalogTable.PutItemAsync(bicycle2); + + var bicycle3 = new Document(); + bicycle3["Id"] = 203; + bicycle3["Title"] = "19-Bike 203"; + bicycle3["Description"] = "203 description"; + bicycle3["BicycleType"] = "Road"; + bicycle3["Brand"] = "Brand-Company B"; + bicycle3["Price"] = 300; + bicycle3["Color"] = new List { "Red", "Green", "Black" }; + bicycle3["ProductCategory"] = "Bike"; + await productCatalogTable.PutItemAsync(bicycle3); + + var bicycle4 = new Document(); + bicycle4["Id"] = 204; + bicycle4["Title"] = "18-Bike 204"; + bicycle4["Description"] = "204 description"; + bicycle4["BicycleType"] = "Mountain"; + bicycle4["Brand"] = "Brand-Company B"; + bicycle4["Price"] = 400; + bicycle4["Color"] = new List { "Red" }; + bicycle4["ProductCategory"] = "Bike"; + await productCatalogTable.PutItemAsync(bicycle4); + + var bicycle5 = new Document(); + bicycle5["Id"] = 205; + bicycle5["Title"] = "20-Title 205"; + bicycle4["Description"] = "205 description"; + bicycle5["BicycleType"] = "Hybrid"; + bicycle5["Brand"] = "Brand-Company C"; + bicycle5["Price"] = 500; + bicycle5["Color"] = new List { "Red", "Black" }; + bicycle5["ProductCategory"] = "Bike"; + await productCatalogTable.PutItemAsync(bicycle5); + } + + private static async Task LoadSampleForums(AmazonDynamoDBClient client) + { + Table forumTable = Table.LoadTable(client, "Forum"); + + var forum1 = new Document(); + forum1["Name"] = "Amazon DynamoDB"; // PK + forum1["Category"] = "Amazon Web Services"; + forum1["Threads"] = 2; + forum1["Messages"] = 4; + forum1["Views"] = 1000; + + await forumTable.PutItemAsync(forum1); + + var forum2 = new Document(); + forum2["Name"] = "Amazon S3"; // PK + forum2["Category"] = "Amazon Web Services"; + forum2["Threads"] = 1; + + await forumTable.PutItemAsync(forum2); + } + + private static async Task LoadSampleThreads(AmazonDynamoDBClient client) + { + Table threadTable = Table.LoadTable(client, "Thread"); + + // Thread 1. + var thread1 = new Document(); + thread1["ForumName"] = "Amazon DynamoDB"; // Hash attribute. + thread1["Subject"] = "DynamoDB Thread 1"; // Range attribute. + thread1["Message"] = "DynamoDB thread 1 message text"; + thread1["LastPostedBy"] = "User A"; + thread1["LastPostedDateTime"] = DateTime.UtcNow.Subtract(new TimeSpan(14, 0, 0, 0)); + thread1["Views"] = 0; + thread1["Replies"] = 0; + thread1["Answered"] = false; + thread1["Tags"] = new List { "index", "primarykey", "table" }; + + await threadTable.PutItemAsync(thread1); + + // Thread 2. + var thread2 = new Document(); + thread2["ForumName"] = "Amazon DynamoDB"; // Hash attribute. + thread2["Subject"] = "DynamoDB Thread 2"; // Range attribute. + thread2["Message"] = "DynamoDB thread 2 message text"; + thread2["LastPostedBy"] = "User A"; + thread2["LastPostedDateTime"] = DateTime.UtcNow.Subtract(new TimeSpan(21, 0, 0, 0)); + thread2["Views"] = 0; + thread2["Replies"] = 0; + thread2["Answered"] = false; + thread2["Tags"] = new List { "index", "primarykey", "rangekey" }; + + await threadTable.PutItemAsync(thread2); + + // Thread 3. + var thread3 = new Document(); + thread3["ForumName"] = "Amazon S3"; // Hash attribute. + thread3["Subject"] = "S3 Thread 1"; // Range attribute. + thread3["Message"] = "S3 thread 3 message text"; + thread3["LastPostedBy"] = "User A"; + thread3["LastPostedDateTime"] = DateTime.UtcNow.Subtract(new TimeSpan(7, 0, 0, 0)); + thread3["Views"] = 0; + thread3["Replies"] = 0; + thread3["Answered"] = false; + thread3["Tags"] = new List { "largeobjects", "multipart upload" }; + await threadTable.PutItemAsync(thread3); + } + + private static async Task LoadSampleReplies(AmazonDynamoDBClient client) + { + Table replyTable = Table.LoadTable(client, "Reply"); + + // Reply 1 - thread 1. + var thread1Reply1 = new Document(); + thread1Reply1["Id"] = "Amazon DynamoDB#DynamoDB Thread 1"; // Hash attribute. + thread1Reply1["ReplyDateTime"] = DateTime.UtcNow.Subtract(new TimeSpan(21, 0, 0, 0)); // Range attribute. + thread1Reply1["Message"] = "DynamoDB Thread 1 Reply 1 text"; + thread1Reply1["PostedBy"] = "User A"; + + await replyTable.PutItemAsync(thread1Reply1); + + // Reply 2 - thread 1. + var thread1reply2 = new Document(); + thread1reply2["Id"] = "Amazon DynamoDB#DynamoDB Thread 1"; // Hash attribute. + thread1reply2["ReplyDateTime"] = DateTime.UtcNow.Subtract(new TimeSpan(14, 0, 0, 0)); // Range attribute. + thread1reply2["Message"] = "DynamoDB Thread 1 Reply 2 text"; + thread1reply2["PostedBy"] = "User B"; + + await replyTable.PutItemAsync(thread1reply2); + + // Reply 3 - thread 1. + var thread1Reply3 = new Document(); + thread1Reply3["Id"] = "Amazon DynamoDB#DynamoDB Thread 1"; // Hash attribute. + thread1Reply3["ReplyDateTime"] = DateTime.UtcNow.Subtract(new TimeSpan(7, 0, 0, 0)); // Range attribute. + thread1Reply3["Message"] = "DynamoDB Thread 1 Reply 3 text"; + thread1Reply3["PostedBy"] = "User B"; + + await replyTable.PutItemAsync(thread1Reply3); + + // Reply 1 - thread 2. + var thread2Reply1 = new Document(); + thread2Reply1["Id"] = "Amazon DynamoDB#DynamoDB Thread 2"; // Hash attribute. + thread2Reply1["ReplyDateTime"] = DateTime.UtcNow.Subtract(new TimeSpan(7, 0, 0, 0)); // Range attribute. + thread2Reply1["Message"] = "DynamoDB Thread 2 Reply 1 text"; + thread2Reply1["PostedBy"] = "User A"; + + await replyTable.PutItemAsync(thread2Reply1); + + // Reply 2 - thread 2. + var thread2Reply2 = new Document(); + thread2Reply2["Id"] = "Amazon DynamoDB#DynamoDB Thread 2"; // Hash attribute. + thread2Reply2["ReplyDateTime"] = DateTime.UtcNow.Subtract(new TimeSpan(1, 0, 0, 0)); // Range attribute. + thread2Reply2["Message"] = "DynamoDB Thread 2 Reply 2 text"; + thread2Reply2["PostedBy"] = "User A"; + + await replyTable.PutItemAsync(thread2Reply2); + } +} \ No newline at end of file diff --git a/AWS/Lab.AwsDDB/test/Lab.AwsDDB.Test/Usings.cs b/AWS/Lab.AwsDDB/test/Lab.AwsDDB.Test/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/AWS/Lab.AwsDDB/test/Lab.AwsDDB.Test/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/AWS/Lab.AwsS3/.gitignore b/AWS/Lab.AwsS3/.gitignore new file mode 100644 index 00000000..eab69e5c --- /dev/null +++ b/AWS/Lab.AwsS3/.gitignore @@ -0,0 +1,1340 @@ +### ASPNETCore template +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# 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 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# 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 + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.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 + +# 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 + +# 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 +# TODO: 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 +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable 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 + +# 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 +node_modules/ +orleans.codegen.cs + +# 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 + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# 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/ + +### Csharp 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/main/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 +*.tlog +*.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 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# 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/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# 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 + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +### ASPNETCore template +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# 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 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# 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 + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.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 + +# 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 + +# 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 +# TODO: 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 +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable 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 + +# 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 +node_modules/ +orleans.codegen.cs + +# 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 + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# 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/ + +### Csharp 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/main/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 +*.tlog +*.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 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# 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/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# 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 + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + 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..55b27f74 --- /dev/null +++ b/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/Lab.Aws.S3.MinIOS3.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + Always + + + + 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..331255cf --- /dev/null +++ b/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/UnitTest1.cs @@ -0,0 +1,341 @@ +using System.Collections.Concurrent; +using System.Reflection; +using Amazon.Runtime; +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.S3.Transfer; +using Testcontainers.Minio; + +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 s3Client = CreateS3Client(); + var response = await s3Client.PutBucketAsync(new PutBucketRequest + { + BucketName = "test-bucket", + }); + } + + [TestMethod] + public async Task 新增一個儲存桶_For_TestContainer() + { + var s3Container = await CreateS3Container(); + var connectionString = s3Container.GetConnectionString(); + var s3Client = CreateS3Client(connectionString); + var response = await s3Client.PutBucketAsync(new PutBucketRequest + { + BucketName = "test-bucket", + }); + } + [TestMethod] + public async Task 上傳檔案到儲存桶_For_TestContainer() + { + var s3Container = await CreateS3Container(); + var connectionString = s3Container.GetConnectionString(); + var s3Client = CreateS3Client(connectionString); + var bucketName = "test-bucket"; + var createBucket = await s3Client.PutBucketAsync(new PutBucketRequest + { + BucketName = bucketName, + }); + + var inputMemory = new MemoryStream(); + await File.Open("上傳.csv", FileMode.Open).CopyToAsync(inputMemory); + var putObjectResponse = await s3Client.PutObjectAsync(new PutObjectRequest + { + BucketName = bucketName, + Key = "上傳.csv", + InputStream = inputMemory, + AutoCloseStream = false, + AutoResetStreamPosition = true, + }); + var getObjectResponse = await s3Client.GetObjectAsync(new GetObjectRequest + { + BucketName = bucketName, + Key = "上傳.csv", + }); + await using var outputStream = File.Create("下載.csv"); + await getObjectResponse.ResponseStream.CopyToAsync(outputStream); + } + + private static async Task CreateS3Container() + { + var username = "AKIAIOSFODNN7EXAMPLE"; + var password = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"; + var minioBuilder = new MinioBuilder() + .WithName("minio") + .WithHostname("localhost") + // .WithImage("quay.io/minio/minio:latest") + // .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(9000)) + .WithUsername(username) + .WithPassword(password) + .WithPortBinding(9000, assignRandomHostPort: true) + // .WithPortBinding(9001, assignRandomHostPort: false) + ; + var minioContainer = minioBuilder.Build(); + await minioContainer.StartAsync().ConfigureAwait(false); + return minioContainer; + } + + [TestMethod] + public async Task 分批上傳檔案() + { + await WriteObjectDataAsync("上傳.csv"); + } + + [TestMethod] + public async Task 分批下載檔案() + { + await ReadObjectDataAsync2("上傳.csv", "下載.csv"); + } + + static async Task ReadObjectDataAsync2(string sourceFile, string outFile) + { + var bucketName = "test-bucket"; + var key = sourceFile; + + try + { + var s3Client = CreateS3Client(); + + long startPosition = 0; + long chunkSize = 4096; // 每次讀取的大小為 4096 bytes + + // 確定檔案的大小 + long fileSize = await GetFileSizeAsync(s3Client, bucketName, key); + + using (var outputStream = new StreamWriter(outFile)) + { + while (startPosition < fileSize) + { + long endPosition = Math.Min(startPosition + chunkSize, fileSize) - 1; + + // 設定 ByteRange + var getObjectRequest = new GetObjectRequest + { + BucketName = bucketName, + Key = key, + ByteRange = new ByteRange(startPosition, endPosition) + }; + + using (var response = await s3Client.GetObjectAsync(getObjectRequest)) + using (var reader = new StreamReader(response.ResponseStream)) + { + // 處理部分檔案內容 + var buffer = new char[chunkSize]; + int bytesRead; + string line = string.Empty; + while ((bytesRead = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + line += new string(buffer, 0, bytesRead); + int newlineIndex = line.IndexOf('\n'); + if (newlineIndex >= 0) + { + await outputStream.WriteAsync(line.Substring(0, newlineIndex + 1)); + line = line.Substring(newlineIndex + 1); + } + } + + // 處理剩餘的部分,如果有 + if (!string.IsNullOrEmpty(line)) + { + await outputStream.WriteLineAsync(line); // 添加換行符號 + } + } + + startPosition += chunkSize; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + } + + private static async Task GetFileSizeAsync(IAmazonS3 s3Client, string bucketName, string objectKey) + { + var metadataRequest = new GetObjectMetadataRequest + { + BucketName = bucketName, + Key = objectKey + }; + + var response = await s3Client.GetObjectMetadataAsync(metadataRequest); + return response.ContentLength; + } + + static async Task ReadObjectDataAsync(string filePath) + { + var s3Client = CreateS3Client(); + var bucketName = "test-bucket"; + + var fileTransferUtilityRequest = new TransferUtilityUploadRequest() + { + }; + var transferUtilityDownloadRequest = new TransferUtilityDownloadRequest + { + BucketName = null, + Key = null, + VersionId = null, + ModifiedSinceDateUtc = default, + UnmodifiedSinceDateUtc = default, + ChecksumMode = null, + ServerSideEncryptionCustomerProvidedKey = null, + ServerSideEncryptionCustomerProvidedKeyMD5 = null, + ServerSideEncryptionCustomerMethod = null, + FilePath = null + }; + var getObjectRequest = new GetObjectRequest + { + BucketName = bucketName, + Key = filePath, + + // PartNumber = 1, + ByteRange = new ByteRange(0, 14096), + }; + var retryCount = 0; + while (true) + { + using var response = await s3Client.GetObjectAsync(getObjectRequest); + using var reader = new StreamReader(response.ResponseStream); + while (true) + { + var line = await reader.ReadLineAsync(); + if (string.IsNullOrEmpty(line)) + { + retryCount++; + break; + } + + // Process the line + Console.WriteLine(line); + retryCount = 0; + } + + if (retryCount >= 2) + { + break; + } + } + } + + static async Task WriteObjectDataAsync(string filePath) + { + var s3Client = CreateS3Client(); + var bucketName = "test-bucket"; + var writerStream = new MemoryStream(); + var writer = new StreamWriter(writerStream) + { + AutoFlush = true + }; + try + { + await using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + using var reader = new StreamReader(fileStream); + var lineCount = 0; + var lineLimit = 10; + var putRequest = new PutObjectRequest() + { + BucketName = bucketName, + Key = filePath, + UseChunkEncoding = true, + AutoCloseStream = false, + AutoResetStreamPosition = true, + }; + while (await reader.ReadLineAsync() is { } line) + { + // todo:可以處理一整批後再寫入到 s3 + await writer.WriteLineAsync(line); + putRequest.InputStream = writerStream; + var response = await s3Client.PutObjectAsync(putRequest); + lineCount++; + } + } + catch (AmazonS3Exception e) + { + Console.WriteLine("Error encountered on server. Message:'{0}' when writing object", e.Message); + } + catch (Exception e) + { + Console.WriteLine("Unknown encountered on server. Message:'{0}' when writing object", e.Message); + } + } + + static AmazonS3Client CreateS3Client(string url = "http://localhost:9000") + { + var credentials = new BasicAWSCredentials("AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"); + + var s3Config = new AmazonS3Config + { + ServiceURL = url, + ForcePathStyle = true + }; + return new AmazonS3Client(credentials, s3Config); + } +} \ 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.Aws.S3.MinIOS3/\344\270\212\345\202\263.csv" "b/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/\344\270\212\345\202\263.csv" new file mode 100644 index 00000000..728912cc --- /dev/null +++ "b/AWS/Lab.AwsS3/Lab.Aws.S3.MinIOS3/\344\270\212\345\202\263.csv" @@ -0,0 +1,5 @@ +id,name,address,age +1d290be4-9573-4e76-9f4c-4daa679caaad,Name1,Address1,45 +6dcd4ce0-df34-4e12-9ec8-2d1bf3be287c,Name2,Address2,20 +8bf2a7cb-2178-4f56-8653-67c58c2f8725,Name3,Address3,68 +8bf2a7cb-2178-4f56-8653-67c58c2f8725,Name4,Address3,68 diff --git a/AWS/Lab.AwsS3/Lab.AwsS3.sln b/AWS/Lab.AwsS3/Lab.AwsS3.sln new file mode 100644 index 00000000..49cbf512 --- /dev/null +++ b/AWS/Lab.AwsS3/Lab.AwsS3.sln @@ -0,0 +1,22 @@ + +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 + .gitignore = .gitignore + 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/AppMetrics/Lab.AppMetricToInfluxDB/WebApi.OWIN.NET48/Web.config b/AppMetrics/Lab.AppMetricToInfluxDB/WebApi.OWIN.NET48/Web.config index 89dc0452..ce4b0c60 100644 --- a/AppMetrics/Lab.AppMetricToInfluxDB/WebApi.OWIN.NET48/Web.config +++ b/AppMetrics/Lab.AppMetricToInfluxDB/WebApi.OWIN.NET48/Web.config @@ -5,64 +5,87 @@ --> - - - - + + + + - - + + - - - - + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/AppMetrics/Lab.AppMetricToInfluxDB/WebApi.OWIN.NET48/packages.config b/AppMetrics/Lab.AppMetricToInfluxDB/WebApi.OWIN.NET48/packages.config index 59490826..8acef1aa 100644 --- a/AppMetrics/Lab.AppMetricToInfluxDB/WebApi.OWIN.NET48/packages.config +++ b/AppMetrics/Lab.AppMetricToInfluxDB/WebApi.OWIN.NET48/packages.config @@ -2,7 +2,7 @@ - + diff --git a/Args/Lab.SpectreConsole/Lab.SpectreConsole.sln b/Args/Lab.SpectreConsole/Lab.SpectreConsole.sln new file mode 100644 index 00000000..d8e6c9a4 --- /dev/null +++ b/Args/Lab.SpectreConsole/Lab.SpectreConsole.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.SpectreConsole", "Lab.SpectreConsole\Lab.SpectreConsole.csproj", "{25BBDAD7-1286-4D7C-9AEA-948810AC146F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {25BBDAD7-1286-4D7C-9AEA-948810AC146F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25BBDAD7-1286-4D7C-9AEA-948810AC146F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25BBDAD7-1286-4D7C-9AEA-948810AC146F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25BBDAD7-1286-4D7C-9AEA-948810AC146F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Args/Lab.SpectreConsole/Lab.SpectreConsole/AddSettings.cs b/Args/Lab.SpectreConsole/Lab.SpectreConsole/AddSettings.cs new file mode 100644 index 00000000..d97e541e --- /dev/null +++ b/Args/Lab.SpectreConsole/Lab.SpectreConsole/AddSettings.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Lab.SpectreConsole; +public class AddPackageCommand : Command +{ + public override int Execute(CommandContext context, AddPackageSettings settings) + { + // Omitted + return 0; + } +} + +public class AddReferenceCommand : Command +{ + public override int Execute(CommandContext context, AddReferenceSettings settings) + { + // Omitted + return 0; + } +} +public class AddSettings : CommandSettings +{ + [CommandArgument(0, "[PROJECT]")] + public string Project { get; set; } +} + +public class AddPackageSettings : AddSettings +{ + [CommandArgument(0, "")] + public string PackageName { get; set; } + + [CommandOption("-v|--version ")] + public string Version { get; set; } +} + +public class AddReferenceSettings : AddSettings +{ + [CommandArgument(0, "")] + public string ProjectReference { get; set; } +} \ No newline at end of file diff --git a/Args/Lab.SpectreConsole/Lab.SpectreConsole/CancellableAsyncCommand.cs b/Args/Lab.SpectreConsole/Lab.SpectreConsole/CancellableAsyncCommand.cs new file mode 100644 index 00000000..33740863 --- /dev/null +++ b/Args/Lab.SpectreConsole/Lab.SpectreConsole/CancellableAsyncCommand.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.Logging; +using Spectre.Console.Cli; + +namespace Lab.SpectreConsole; + +public abstract class CancellableAsyncCommand : AsyncCommand + where TSettings : CommandSettings +{ + private readonly ILogger> _logger; + + protected CancellableAsyncCommand(ILogger> logger) + { + this._logger = logger; + } + + public abstract Task ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellation); + + public override async Task ExecuteAsync(CommandContext context, TSettings settings) + { + using var cancellationSource = new CancellationTokenSource(); + + Console.CancelKeyPress += OnCancelKeyPress; + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + + using var _ = cancellationSource.Token.Register( + () => + { + AppDomain.CurrentDomain.ProcessExit -= OnProcessExit; + Console.CancelKeyPress -= OnCancelKeyPress; + } + ); + int exitCode = -1; + try + { + this._logger.LogInformation("執行任務中..."); + var executeTask = this.ExecuteAsync(context, settings, cancellationSource.Token); + exitCode = await executeTask; + this._logger.LogInformation("執行完成!!!"); + AppDomain.CurrentDomain.ProcessExit -= OnProcessExit; + Console.CancelKeyPress -= OnCancelKeyPress; + } + catch (OperationCanceledException) + { + exitCode = 0; + } + catch (Exception e) + { + this._logger.LogError(e, "執行命令時發生非預期的錯誤"); + } + + return exitCode; + + void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e) + { + // NOTE: cancel event, don't terminate the process + e.Cancel = true; + + cancellationSource.Cancel(); + } + + void OnProcessExit(object? sender, EventArgs e) + { + if (cancellationSource.IsCancellationRequested) + { + // NOTE: SIGINT (cancel key was pressed, this shouldn't ever actually hit however, as we remove the event handler upon cancellation of the `cancellationSource`) + return; + } + + cancellationSource.Cancel(); + } + } +} \ No newline at end of file diff --git a/Args/Lab.SpectreConsole/Lab.SpectreConsole/FileSizeAsyncCommand.cs b/Args/Lab.SpectreConsole/Lab.SpectreConsole/FileSizeAsyncCommand.cs new file mode 100644 index 00000000..fafa5355 --- /dev/null +++ b/Args/Lab.SpectreConsole/Lab.SpectreConsole/FileSizeAsyncCommand.cs @@ -0,0 +1,55 @@ +using System.ComponentModel; +using Microsoft.Extensions.Logging; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Lab.SpectreConsole; + +internal sealed class FileSizeAsyncCommand : CancellableAsyncCommand +{ + internal sealed class Settings : CommandSettings + { + [Description("Path to search. Defaults to current directory.")] + [CommandArgument(0, "[searchPath]")] + public string? SearchPath { get; init; } + + [CommandOption("-p|--pattern")] + public string? SearchPattern { get; init; } + + [CommandOption("--hidden")] + [DefaultValue(true)] + public bool IncludeHidden { get; init; } + } + + public override async Task ExecuteAsync(CommandContext context, + Settings settings, + CancellationToken cancellation) + { + await Task.Delay(5000, cancellation); + + var searchOptions = new EnumerationOptions + { + AttributesToSkip = settings.IncludeHidden + ? FileAttributes.Hidden | FileAttributes.System + : FileAttributes.System + }; + + var searchPattern = settings.SearchPattern ?? "*.*"; + var searchPath = settings.SearchPath ?? Directory.GetCurrentDirectory(); + var files = new DirectoryInfo(searchPath) + .GetFiles(searchPattern, searchOptions); + + var totalFileSize = files + .Sum(fileInfo => fileInfo.Length); + + AnsiConsole.MarkupLine( + $"Total file size for [green]{searchPattern}[/] files in [green]{searchPath}[/]: [blue]{totalFileSize:N0}[/] bytes"); + + return 0; + } + + public FileSizeAsyncCommand(ILogger> logger) + : base(logger) + { + } +} \ No newline at end of file diff --git a/Args/Lab.SpectreConsole/Lab.SpectreConsole/Lab.SpectreConsole.csproj b/Args/Lab.SpectreConsole/Lab.SpectreConsole/Lab.SpectreConsole.csproj new file mode 100644 index 00000000..784b0165 --- /dev/null +++ b/Args/Lab.SpectreConsole/Lab.SpectreConsole/Lab.SpectreConsole.csproj @@ -0,0 +1,23 @@ + + + + Exe + net7.0 + enable + enable + app + + + + + + + + + + + + + + + diff --git a/Args/Lab.SpectreConsole/Lab.SpectreConsole/Program.cs b/Args/Lab.SpectreConsole/Lab.SpectreConsole/Program.cs new file mode 100644 index 00000000..07291e12 --- /dev/null +++ b/Args/Lab.SpectreConsole/Lab.SpectreConsole/Program.cs @@ -0,0 +1,42 @@ +// See https://aka.ms/new-console-template for more information + +using Lab.SpectreConsole; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Templates; +using Spectre.Console.Extensions.Hosting; + +// var formatter = new CompactJsonFormatter(); +var formatter = new ExpressionTemplate( + "{ {_t: @t, _msg: @m, _props: @p} }\n"); +Log.Logger = new LoggerConfiguration() + // .MinimumLevel.Information() + .Enrich.FromLogContext() + .WriteTo.Console(formatter) + .WriteTo.File(formatter, "logs/host-.txt", rollingInterval: RollingInterval.Hour) + .CreateBootstrapLogger(); + +var currentDomain = AppDomain.CurrentDomain; +currentDomain.UnhandledException += (_, eventArgs) => +{ + var e = (Exception)eventArgs.ExceptionObject; + Log.Error(e, "執行命令時發生非預期的錯誤"); +}; + +try +{ + Log.Information("程序開始"); + await Host.CreateDefaultBuilder(args) + .UseSerilog() + .UseConsoleLifetime() + .UseSpectreConsole(config => { config.AddCommand("filesize"); }) + .RunConsoleAsync() + ; + Console.WriteLine("程序結束"); + return Environment.ExitCode; +} +catch (Exception e) +{ + Log.Error(e, "執行命令時發生非預期的錯誤"); + return -1; +} \ No newline at end of file diff --git a/Args/Lab.SysCommand/Lab.SysCommand.sln b/Args/Lab.SysCommand/Lab.SysCommand.sln new file mode 100644 index 00000000..70953a42 --- /dev/null +++ b/Args/Lab.SysCommand/Lab.SysCommand.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.SysCommand", "Lab.SysCommand\Lab.SysCommand.csproj", "{FD82FD27-B9AF-4E34-8021-C5B3A1C34CC5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FD82FD27-B9AF-4E34-8021-C5B3A1C34CC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD82FD27-B9AF-4E34-8021-C5B3A1C34CC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD82FD27-B9AF-4E34-8021-C5B3A1C34CC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD82FD27-B9AF-4E34-8021-C5B3A1C34CC5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Args/Lab.SysCommand/Lab.SysCommand/Lab.SysCommand.csproj b/Args/Lab.SysCommand/Lab.SysCommand/Lab.SysCommand.csproj new file mode 100644 index 00000000..b66d7381 --- /dev/null +++ b/Args/Lab.SysCommand/Lab.SysCommand/Lab.SysCommand.csproj @@ -0,0 +1,16 @@ + + + + Exe + net7.0 + enable + enable + app + + + + + + + + diff --git a/Args/Lab.SysCommand/Lab.SysCommand/Program.cs b/Args/Lab.SysCommand/Lab.SysCommand/Program.cs new file mode 100644 index 00000000..c7410ae9 --- /dev/null +++ b/Args/Lab.SysCommand/Lab.SysCommand/Program.cs @@ -0,0 +1,11 @@ +// See https://aka.ms/new-console-template for more information + +using System.CommandLine; + +var rootCommand = new RootCommand(); +var sub1Command = new Command("sub1", "First-level subcommand"); +rootCommand.Add(sub1Command); +var sub1aCommand = new Command("sub1a", "Second level subcommand"); +sub1Command.Add(sub1aCommand); + +await rootCommand.InvokeAsync(args); 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.EnvGenerator/Lab.EnvGenerator.sln b/Configuration/Lab.EnvGenerator/Lab.EnvGenerator.sln new file mode 100644 index 00000000..fb45d981 --- /dev/null +++ b/Configuration/Lab.EnvGenerator/Lab.EnvGenerator.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.EnvGeneratorCli", "Lab.EnvGeneratorCli\Lab.EnvGeneratorCli.csproj", "{FF0E7D63-9FCA-4062-9DE2-1BF6A92A7100}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FF0E7D63-9FCA-4062-9DE2-1BF6A92A7100}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF0E7D63-9FCA-4062-9DE2-1BF6A92A7100}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF0E7D63-9FCA-4062-9DE2-1BF6A92A7100}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF0E7D63-9FCA-4062-9DE2-1BF6A92A7100}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/ConvertEnvCommand.cs b/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/ConvertEnvCommand.cs new file mode 100644 index 00000000..d9b26b29 --- /dev/null +++ b/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/ConvertEnvCommand.cs @@ -0,0 +1,86 @@ +using Spectre.Console.Cli; + +namespace Lab.EnvGeneratorCli; + +internal sealed class ConvertEnvCommand : Command +{ + public sealed class Settings : CommandSettings + { + [CommandOption("--env")] + public string? Environment { get; init; } + } + + public override int Execute(CommandContext context, Settings settings) + { + // 讀取 env.template 檔案 + var envTemplate = File.ReadAllLines("env.template"); + + var env = settings.Environment; + + var outputFileName = $"app.{env}.env"; + + // 解析 env.template 檔案 + var contents = ParseEnvTemplate(envTemplate, env); + GenerateEnvFile(contents, outputFileName); + Console.WriteLine($"Generated {outputFileName}."); + + return 0; + } + + private static void GenerateEnvFile(Dictionary settings, string outputFileName) + { + using var writer = new StreamWriter(outputFileName); + foreach (var setting in settings) + { + writer.WriteLine($"{setting.Key}={setting.Value}"); + } + } + + private static Dictionary ParseEnvTemplate(string[] templateLines, string env) + { + var result = new Dictionary(); + var currentSection = ""; + foreach (var line in templateLines) + { + if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) + { + continue; + } + + // 找出 section + if (line.StartsWith("[") && line.EndsWith("]")) + { + currentSection = line.Trim('[', ']'); + continue; + } + + var parts = line.Split('=', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 2) + { + continue; + } + + var key = parts[0].Trim(); + var value = parts[1].Trim(); + if (parts.Length > 2) + { + // 被分割的部分重新組合 + value = string.Join("=", parts.Skip(1)).Trim(); + } + + // 優先取環境變數的值 + if (key == env) + { + result[currentSection] = value; + } + + // 若沒有環境變數的值,則取 default + else if (key == "default") + { + result.TryAdd(currentSection, value); + } + } + + return result; + } +} \ No newline at end of file diff --git a/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/Lab.EnvGeneratorCli.csproj b/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/Lab.EnvGeneratorCli.csproj new file mode 100644 index 00000000..215ad69f --- /dev/null +++ b/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/Lab.EnvGeneratorCli.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + enable + enable + EnvGeneratorCli + EnvGeneratorCli + cli + + + + + Always + + + + + + + + diff --git a/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/Program.cs b/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/Program.cs new file mode 100644 index 00000000..884fba54 --- /dev/null +++ b/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/Program.cs @@ -0,0 +1,19 @@ +using Spectre.Console.Cli; + +namespace Lab.EnvGeneratorCli; + +internal class Program +{ + private static void Main(string[] args) + { + var app = new CommandApp(); + app.Configure(config => + { + config.AddCommand("convert") + .WithDescription("convert a file.") + .WithExample("convert", "--env", "qa"); + }); + + app.Run(args); + } +} \ No newline at end of file diff --git a/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/env.template b/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/env.template new file mode 100644 index 00000000..2cae63d3 --- /dev/null +++ b/Configuration/Lab.EnvGenerator/Lab.EnvGeneratorCli/env.template @@ -0,0 +1,12 @@ +[DefaultConnection] +default=local +qa=Server=qa;Database=proddb;User Id=produser;Password=prodpassword; +prod=Server=prodserver;Database=proddb;User Id=produser;Password=prodpassword; + +[Ports] +default=8001, 8001, 8002 + +[Allowed] +default=false +qa=true +prod=false \ No newline at end of file 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/Lab.MsDI.Lazy/Lab.MsDI.Lazy.sln b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy.sln new file mode 100644 index 00000000..77587338 --- /dev/null +++ b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.MsDI.Lazy", "Lab.MsDI.Lazy\Lab.MsDI.Lazy.csproj", "{33CA20D4-BF77-46F7-9BEB-6C75E0717EC3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {33CA20D4-BF77-46F7-9BEB-6C75E0717EC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33CA20D4-BF77-46F7-9BEB-6C75E0717EC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33CA20D4-BF77-46F7-9BEB-6C75E0717EC3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33CA20D4-BF77-46F7-9BEB-6C75E0717EC3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Controllers/DemoController.cs b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Controllers/DemoController.cs new file mode 100644 index 00000000..031c244b --- /dev/null +++ b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Controllers/DemoController.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.MsDI.Lazy.Controllers; + +[ApiController] +[Route("[controller]")] +public class DemoController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IService _service; + public DemoController(ILogger logger, IService service) + { + this._logger = logger; + this._service = service; + } + + [HttpGet(Name = "GetDemo")] + public ActionResult Get() + { + return this.Ok(this._service.Get()); + } +} \ No newline at end of file diff --git a/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Lab.MsDI.Lazy.csproj b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Lab.MsDI.Lazy.csproj new file mode 100644 index 00000000..aad5fe67 --- /dev/null +++ b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Lab.MsDI.Lazy.csproj @@ -0,0 +1,16 @@ + + + + net7.0 + enable + enable + + + + + + + + + + diff --git a/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Program.cs b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Program.cs new file mode 100644 index 00000000..c41ac9a6 --- /dev/null +++ b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Program.cs @@ -0,0 +1,57 @@ +using Lab.MsDI.Lazy; +using LazyProxy; +using LazyProxy.ServiceProvider; + +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.AddScoped(); +// builder.Services.AddScoped(); + +// builder.Services.AddScoped(); +// +// builder.Services.AddScoped(p => +// { +// var serviceA = new Lazy(() => p.GetService()); +// var serviceB = new Lazy(() => p.GetService()); +// return new ServiceLazy(serviceA, serviceB); +// }); + +// builder.Services.AddScoped(p => +// { +// var lazyProxy = LazyProxyBuilder.CreateInstance(() => +// { +// var serviceA = p.GetService(); +// var serviceB = p.GetService(); +// return new Service(serviceA, serviceB); +// }); +// return lazyProxy; +// }); + +builder.Services.AddLazyScoped(); +builder.Services.AddLazyScoped(); +builder.Services.AddLazyScoped(); + +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/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Properties/launchSettings.json b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Properties/launchSettings.json new file mode 100644 index 00000000..82121723 --- /dev/null +++ b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:36643", + "sslPort": 44334 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5102", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7029;http://localhost:5102", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Service.cs b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Service.cs new file mode 100644 index 00000000..c62a8126 --- /dev/null +++ b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/Service.cs @@ -0,0 +1,54 @@ +namespace Lab.MsDI.Lazy; + +public interface IService +{ + string Get(); +} + +public class Service : IService +{ + private readonly IServiceA _serviceA; + private readonly IServiceB _serviceB; + + public Service(IServiceA serviceA, + IServiceB serviceB) + { + this._serviceA = serviceA; + this._serviceB = serviceB; + } + + public string Get() + { + var random = new Random().Next(1, 10); + if (random % 2 == 0) + { + return this._serviceB.Get(); + } + + return this._serviceA.Get(); + } +} + +public interface IServiceA +{ + string Get(); +} + +public class ServiceA : IServiceA +{ + public ServiceA() => Console.WriteLine("Create instance for ServiceA"); + + public string Get() => "ServiceA"; +} + +public interface IServiceB +{ + string Get(); +} + +public class ServiceB : IServiceB +{ + public ServiceB() => Console.WriteLine("Create instance for ServiceB"); + + public string Get() => "ServiceB"; +} \ No newline at end of file diff --git a/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/ServiceLazy.cs b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/ServiceLazy.cs new file mode 100644 index 00000000..0b0f3c68 --- /dev/null +++ b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/ServiceLazy.cs @@ -0,0 +1,25 @@ +namespace Lab.MsDI.Lazy; + +public class ServiceLazy : IService +{ + private readonly Lazy _serviceA; + private readonly Lazy _serviceB; + + public ServiceLazy(Lazy serviceA, + Lazy serviceB) + { + this._serviceA = serviceA; + this._serviceB = serviceB; + } + + public string Get() + { + var random = new Random().Next(1, 10); + if (random % 2 == 0) + { + return this._serviceB.Value.Get(); + } + + return this._serviceA.Value.Get(); + } +} \ No newline at end of file diff --git a/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/appsettings.Development.json b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/appsettings.json b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/DI/Lab.MsDI/Lab.MsDI.Lazy/Lab.MsDI.Lazy/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} 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.MsDI/WinFormNet48/Form1.cs b/DI/Lab.MsDI/WinFormNet48/Form1.cs index 3c3e1406..f49d6289 100644 --- a/DI/Lab.MsDI/WinFormNet48/Form1.cs +++ b/DI/Lab.MsDI/WinFormNet48/Form1.cs @@ -4,8 +4,22 @@ namespace WinFormNet48 { + public class BaseForm : Form + { + public BaseForm() + { + var serviceProvider = DependencyInjectionConfig.ServiceProvider; + using (var serviceScope = serviceProvider.CreateScope()) + { + var work = serviceScope.ServiceProvider.GetRequiredService(); + } + } + } + public partial class Form1 : Form { + int _counter = 1; + public Form1() { this.InitializeComponent(); @@ -14,8 +28,13 @@ public Form1() private void button1_Click(object sender, EventArgs e) { var serviceProvider = DependencyInjectionConfig.ServiceProvider; - var work = serviceProvider.GetRequiredService(); - Console.WriteLine(work.Get()); + using (var serviceScope = serviceProvider.CreateScope()) + { + var work = serviceScope.ServiceProvider.GetRequiredService(); + Console.WriteLine($"{this._counter}=>\r\n" + work.Get()); + } + + this._counter++; } } } \ 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/Error Handler/Without Exception/ErrorHandler.sln b/Error Handler/Without Exception/ErrorHandler.sln new file mode 100644 index 00000000..1c4d184a --- /dev/null +++ b/Error Handler/Without Exception/ErrorHandler.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ErrorHandler.API", "Lab.ErrorHandler.API\Lab.ErrorHandler.API.csproj", "{E1409B61-F8DB-4533-BEA4-0B83B7F59491}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E1409B61-F8DB-4533-BEA4-0B83B7F59491}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1409B61-F8DB-4533-BEA4-0B83B7F59491}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1409B61-F8DB-4533-BEA4-0B83B7F59491}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1409B61-F8DB-4533-BEA4-0B83B7F59491}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Controllers/FailureObjectResult.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Controllers/FailureObjectResult.cs new file mode 100644 index 00000000..26c7e2f7 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Controllers/FailureObjectResult.cs @@ -0,0 +1,16 @@ +using Lab.ErrorHandler.API.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.ErrorHandler.API.Controllers; + +public class FailureObjectResult : ObjectResult +{ + public FailureObjectResult(Failure failure, int statusCode = StatusCodes.Status400BadRequest) + : base(failure) + { + this.StatusCode = statusCode; + // Failure.Exception 已經使用 [JsonIgnore],不會再回傳給調用端 + // this.Value = failure.WithoutException(); + this.Value = failure; + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Controllers/GenericController.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Controllers/GenericController.cs new file mode 100644 index 00000000..40207438 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Controllers/GenericController.cs @@ -0,0 +1,72 @@ +using System.Diagnostics; +using Lab.ErrorHandler.API.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.ErrorHandler.API.Controllers; + +public class GenericController : ControllerBase +{ + public Dictionary FailureCodeLookup => s_failureCodeLookupLazy.Value; + + private static readonly Lazy> s_failureCodeLookupLazy = new(CreateFailureCodeLookup); + + private static Dictionary CreateFailureCodeMappings() + { + //用關鍵字定義錯誤代碼 + return new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + { "error", StatusCodes.Status500InternalServerError }, + { "invalid", StatusCodes.Status400BadRequest }, + { "notfound", StatusCodes.Status404NotFound }, + { "concurrency", StatusCodes.Status429TooManyRequests }, + { "conflict", StatusCodes.Status404NotFound }, + }; + } + + [NonAction] + public FailureObjectResult FailureContent(Failure failure) + { + if (string.IsNullOrWhiteSpace(failure.TraceId)) + { + failure.TraceId = Activity.Current?.Id ?? this.HttpContext.TraceIdentifier; + } + + if (FailureCodeLookup.TryGetValue(failure.Code, out int statusCode)) + { + return new FailureObjectResult(failure, statusCode); + } + + return new FailureObjectResult(failure); + } + + private static Dictionary CreateFailureCodeLookup() + { + var result = new Dictionary(); + var type = typeof(FailureCode); + var names = Enum.GetNames(type); + var failureMappings = CreateFailureCodeMappings(); + foreach (var name in names) + { + var failureCode = FailureCode.Parse(name); + var isDefined = false; + foreach (var mapping in failureMappings) + { + var key = mapping.Key; + var statusCode = mapping.Value; + if (name.Contains(key, StringComparison.OrdinalIgnoreCase)) + { + isDefined = true; + result.Add(failureCode, statusCode); + break; + } + } + + if (isDefined == false) + { + result.Add(failureCode, StatusCodes.Status500InternalServerError); + } + } + + return result; + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Controllers/MembersController.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Controllers/MembersController.cs new file mode 100644 index 00000000..02f113b0 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Controllers/MembersController.cs @@ -0,0 +1,39 @@ +using Lab.ErrorHandler.API.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.ErrorHandler.API.Controllers; + +[ApiController] +[Route("[controller]")] +public class MembersController : GenericController +{ + private readonly ILogger _logger; + private readonly MemberService3 _memberService; + + public MembersController(ILogger logger, + MemberService3 memberService) + { + this._logger = logger; + this._memberService = memberService; + } + + [Produces("application/json")] + [HttpPost("{memberId}/bind-cellphone", Name = "BindCellphone")] + [ProducesResponseType(typeof(Failure), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task Post(int memberId, + BindCellphoneRequest request, + CancellationToken cancel = default) + { + request.MemberId = memberId; + var bindCellphoneResult = + await this._memberService.BindCellphoneAsync(request, cancel); + if (bindCellphoneResult.Failure != null) + { + this._logger.LogInformation(500, "Bind cellphone failure:{@Failure}", bindCellphoneResult.Failure); + return this.FailureContent(bindCellphoneResult.Failure); + } + + return this.NoContent(); + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Extensions/ValidationResultExtension.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Extensions/ValidationResultExtension.cs new file mode 100644 index 00000000..70bc24ca --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Extensions/ValidationResultExtension.cs @@ -0,0 +1,25 @@ +using FluentValidation.Results; +using Lab.ErrorHandler.API.Models; + +namespace Lab.ErrorHandler.API.Extensions; + +static class ValidationResultExtension +{ + public static Failure ToFailure(this ValidationResult validateResult) + { + if (validateResult.IsValid) + { + return null; + } + + var errors = validateResult.Errors + .ToDictionary(p => p.PropertyName, p => p.ErrorMessage); + var failure = new Failure() + { + Code = FailureCode.InputInvalid, + Message = "input invalid", + Data = errors, + }; + return failure; + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Filters/ModelValidationAttribute.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Filters/ModelValidationAttribute.cs new file mode 100644 index 00000000..4ed3cc49 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Filters/ModelValidationAttribute.cs @@ -0,0 +1,73 @@ +using System.Diagnostics; +using Lab.ErrorHandler.API.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Lab.ErrorHandler.API.Filters; + +public class ModelValidationAttribute : ActionFilterAttribute +{ + public override void OnActionExecuting(ActionExecutingContext actionContext) + { + // if (actionContext.Result != null) + // { + // return; + // } + // + // if (actionContext.ModelState.IsValid) + // { + // return; + // } + + var traceId = Activity.Current?.Id ?? actionContext.HttpContext.TraceIdentifier; + + //處理 JSON Path + var jsonPathKeys = actionContext.ModelState.Keys.Where(e => e.StartsWith("$.")).ToList(); + if (jsonPathKeys.Count > 0) + { + var errorData = new Dictionary(); + foreach (var key in jsonPathKeys) + { + var normalizedKey = key.Substring(2); + foreach (var error in actionContext.ModelState[key].Errors) + { + if (error.Exception != null) + { + actionContext.ModelState.TryAddModelException(normalizedKey, error.Exception); + } + + actionContext.ModelState.TryAddModelError(normalizedKey, "The provided value is not valid."); + errorData.Add(normalizedKey, error.ErrorMessage); + } + + actionContext.ModelState.Remove(key); + } + + //複寫錯誤內容 + actionContext.Result = new BadRequestObjectResult(new Failure + { + Code = FailureCode.InputInvalid, + Message = "enum invalid", + Data = errorData, + TraceId = traceId + }); + return; + } + + var errors = actionContext.ModelState + .Where(p => p.Value.ValidationState == ModelValidationState.Invalid) + .ToDictionary( + p => p.Key, + p => p.Value.Errors.Select(e => e.ErrorMessage).ToList()); + + //複寫錯誤內容 + actionContext.Result = new BadRequestObjectResult(new Failure() + { + Code = FailureCode.InputInvalid, + Message = "input invalid", + Data = errors, + TraceId = traceId + }); + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Lab.ErrorHandler.API.csproj b/Error Handler/Without Exception/Lab.ErrorHandler.API/Lab.ErrorHandler.API.csproj new file mode 100644 index 00000000..c8414612 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Lab.ErrorHandler.API.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService.cs new file mode 100644 index 00000000..0365637d --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService.cs @@ -0,0 +1,126 @@ +using System.Data; +using FluentValidation; +using Lab.ErrorHandler.API.Extensions; +using Lab.ErrorHandler.API.Models; + +namespace Lab.ErrorHandler.API; + +public class MemberService +{ + private readonly IValidator _validator; + + public MemberService(IValidator validator) + { + this._validator = validator; + } + + //一個方法有多種可能的 Failure + public async Task BindCellphoneAsync(BindCellphoneRequest request, + CancellationToken cancel = default) + { + var validationResult = await this._validator.ValidateAsync(request, cancel); + if (validationResult.IsValid == false) + { + return false; + } + + try + { + //找不到會員 + var getMemberResult = await this.GetMemberAsync(request.MemberId, cancel); + } + catch (Exception e) + { + } + + try + { + //手機格式無效 + var validateCellphoneResult = await this.ValidateCellphoneAsync(request.Cellphone, cancel); + } + catch (Exception e) + { + } + + try + { + //資料衝突,手機已經被綁定 + var saveChangeResult = await this.SaveChangeAsync(request, cancel); + } + catch (Exception e) + { + } + + return true; + } + + public async Task<(Failure Failure, bool Data)> SaveChangeAsync(BindCellphoneRequest request, + CancellationToken cancel = default) + { + throw new DBConcurrencyException("insert data row concurrency error."); + } + + public async Task ValidateCellphoneAsync(string cellphone, + CancellationToken cancel = default) + { + throw new Exception("Cellphone format invalid."); + } + + // public async Task GetMemberAsync(int memberId, + // CancellationToken cancel = default) + // { + // try + // { + // throw new Exception("Member not found."); + // } + // catch (Exception e) + // { + // throw; + // } + // } + public async Task<(Failure Failure, GetMemberResult Data)> GetMemberAsync(int memberId, + CancellationToken cancel = default) + { + try + { + if (memberId == 1) + { + return (new Failure + { + Code = FailureCode.MemberNotFound, + Message = "member not found.", + }, null); + } + + throw new Exception("Member not found."); + } + catch (Exception e) + { + return (new Failure + { + Code = FailureCode.DbError, + Message = e.Message, + Data = memberId, + Exception = e, + }, null); + } + } + + //具有多個 Detail 的 Failure + public async Task<(Failure Failure, bool Data)> CreateMemberAsync(CreateMemberRequest request, + CancellationToken cancel = default) + { + var failure = new Failure() + { + Code = FailureCode.InputInvalid, + Message = "view detail errors", + Details = new List() + { + new(code: FailureCode.InputInvalid, message: "Input invalid."), + new(code: FailureCode.CellphoneFormatInvalid, message: "Cellphone format invalid."), + new(code: FailureCode.DataConflict, message: "Member already exist."), + } + }; + return (failure, false); + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService1.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService1.cs new file mode 100644 index 00000000..9c90df19 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService1.cs @@ -0,0 +1,123 @@ +using System.Data; +using FluentValidation; +using Lab.ErrorHandler.API.Extensions; +using Lab.ErrorHandler.API.Models; + +namespace Lab.ErrorHandler.API; + +public class MemberService1 +{ + private readonly IValidator _validator; + + public MemberService1(IValidator validator) + { + this._validator = validator; + } + + //一個方法有多種可能的 Failure + public async Task<(Failure Failure, bool Data)> BindCellphoneAsync(BindCellphoneRequest request, + CancellationToken cancel = default) + { + var validationResult = await this._validator.ValidateAsync(request, cancel); + if (validationResult.IsValid == false) + { + return (validationResult.ToFailure(), false); + } + + //找不到會員 + var getMemberResult = await this.GetMemberAsync(request.MemberId, cancel); + if (getMemberResult.Failure != null) + { + return (getMemberResult.Failure, false); + } + + //手機格式無效 + var validateCellphoneResult = await this.ValidateCellphoneAsync(getMemberResult.Data.Cellphone, cancel); + if (validateCellphoneResult.Failure != null) + { + return validateCellphoneResult; + } + + //資料衝突,手機已經被綁定 + var saveChangeResult = await this.SaveChangeAsync(request, cancel); + + return saveChangeResult; + } + + public async Task<(Failure Failure, bool Data)> SaveChangeAsync(BindCellphoneRequest request, + CancellationToken cancel = default) + { + try + { + throw new DBConcurrencyException("insert data row concurrency error."); + } + catch (Exception e) + { + return (new Failure + { + Code = FailureCode.DataConcurrency, + Message = e.Message, + Exception = e, + Data = request + }, false); + } + } + + public async Task<(Failure Failure, bool Data)> ValidateCellphoneAsync(string cellphone, + CancellationToken cancel = default) + { + return (new Failure + { + Code = FailureCode.CellphoneFormatInvalid, + Message = "Cellphone format invalid.", + Data = cellphone + }, false); + } + + public async Task<(Failure Failure, GetMemberResult Data)> GetMemberAsync(int memberId, + CancellationToken cancel = default) + { + try + { + if (memberId == 1) + { + //模擬找不到資料所回傳的失敗 + return (new Failure + { + Code = FailureCode.MemberNotFound, + Message = "member not found.", + }, null); + } + + throw new Exception($"can not connect db."); + } + catch (Exception e) + { + return (new Failure + { + Code = FailureCode.DbError, + Message = e.Message, + Data = memberId, + Exception = e, + }, null); + } + } + + //具有多個 Detail 的 Failure + public async Task<(Failure Failure, bool Data)> CreateMemberAsync(CreateMemberRequest request, + CancellationToken cancel = default) + { + var failure = new Failure() + { + Code = FailureCode.InputInvalid, + Message = "view detail errors", + Details = new List() + { + new(code: FailureCode.InputInvalid, message: "Input invalid."), + new(code: FailureCode.CellphoneFormatInvalid, message: "Cellphone format invalid."), + new(code: FailureCode.DataConflict, message: "Member already exist."), + } + }; + return (failure, false); + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService2.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService2.cs new file mode 100644 index 00000000..f86758b6 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService2.cs @@ -0,0 +1,141 @@ +using System.Data; +using FluentValidation; +using Lab.ErrorHandler.API.Extensions; +using Lab.ErrorHandler.API.Models; + +namespace Lab.ErrorHandler.API; + +public class MemberService2 +{ + private MemberWorkflow _workflow; + + public MemberService2(MemberWorkflow workflow) + { + this._workflow = workflow; + } + + //一個方法有多種可能的 Failure + public async Task<(Failure Failure, bool Data)> BindCellphoneAsync(BindCellphoneRequest request, + CancellationToken cancel = default) + { + // var result = + // await (await (await (await this._workflow.ValidateModelAsync(request, cancel)) + // .GetMemberAsync(request.MemberId, cancel)) + // .ValidateCellphone(request.Cellphone, cancel)).SaveChangeAsync(request, cancel); + + var result = await _workflow.ValidateModelAsync(request, cancel) + .Then(p => _workflow.GetMemberAsync(request.MemberId, cancel)) + .Then(p => _workflow.ValidateCellphone(request.Cellphone, cancel)) + .Then(p => _workflow.SaveChangeAsync(request, cancel)) + ; + if (result.Failure != null) + { + return (result.Failure, false); + } + + return (null, true); + } + + public class MemberWorkflow + { + private readonly IValidator _validator; + + public MemberWorkflow(IValidator validator) + { + this._validator = validator; + } + + public Failure Failure { get; private set; } + + public async Task ValidateModelAsync(BindCellphoneRequest request, + CancellationToken cancel = default) + { + var validationResult = await this._validator.ValidateAsync(request, cancel); + if (validationResult.IsValid == false) + { + this.Failure = validationResult.ToFailure(); + } + + return this; + } + + public async Task SaveChangeAsync(BindCellphoneRequest request, + CancellationToken cancel = default) + { + if (this.Failure != null) + { + return this; + } + + try + { + throw new DBConcurrencyException("insert data row concurrency error."); + } + catch (Exception e) + { + this.Failure = new Failure + { + Code = FailureCode.DataConcurrency, + Message = e.Message, + Exception = e, + Data = request + }; + } + + return this; + } + + public async Task ValidateCellphone(string cellphone, + CancellationToken cancel = default) + { + if (this.Failure != null) + { + return this; + } + + this.Failure = new Failure + { + Code = FailureCode.CellphoneFormatInvalid, + Message = "Cellphone format invalid.", + Data = cellphone + }; + return this; + } + + public async Task GetMemberAsync(int memberId, + CancellationToken cancel = default) + { + if (this.Failure != null) + { + return this; + } + + try + { + if (memberId == 1) + { + this.Failure = new Failure + { + Code = FailureCode.MemberNotFound, + Message = "member not found.", + }; + return this; + } + + throw new Exception($"can not connect db."); + } + catch (Exception e) + { + this.Failure = new Failure + { + Code = FailureCode.DbError, + Message = e.Message, + Data = memberId, + Exception = e, + }; + } + + return this; + } + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService3.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService3.cs new file mode 100644 index 00000000..372666d7 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService3.cs @@ -0,0 +1,107 @@ +using System.Data; +using FluentValidation; +using Lab.ErrorHandler.API.Extensions; +using Lab.ErrorHandler.API.Models; + +namespace Lab.ErrorHandler.API; + +public class MemberService3 +{ + private readonly IValidator _validator; + + public MemberService3(IValidator validator) + { + this._validator = validator; + } + + //一個方法有多種可能的 Failure + // public async Task<(Failure Failure, bool Data)> BindCellphoneAsync(BindCellphoneRequest request, + // CancellationToken cancel = default) => + // await this.ValidateModelAsync(request, cancel) + // .ThenWithFailureAsync(p => p.Failure != null + // ? Task.FromResult((p.Failure, false)) + // : this.ValidateCellphoneAsync(request.Cellphone, cancel)) + // .ThenWithFailureAsync(p => p.Failure != null + // ? Task.FromResult((p.Failure, false)) + // : this.GetMemberAsync(request.MemberId, cancel)) + // .ThenWithFailureAsync(p => p.Failure != null + // ? Task.FromResult((p.Failure, false)) + // : this.SaveChangeAsync(request, cancel)) + // ; + public async Task<(Failure Failure, bool Data)> BindCellphoneAsync(BindCellphoneRequest request, + CancellationToken cancel = default) => + await this.ValidateModelAsync(request, cancel) + .WhenSuccess(p => this.GetMemberAsync(request.MemberId, cancel)) + .WhenSuccess(p => this.ValidateCellphoneAsync(p.Cellphone, cancel)) + .WhenSuccess(p => this.SaveChangeAsync(request, cancel)); + + public async Task<(Failure Failure, bool Data)> ValidateModelAsync(BindCellphoneRequest request, + CancellationToken cancel = default) + { + var validationResult = await this._validator.ValidateAsync(request, cancel); + if (validationResult.IsValid == false) + { + return (validationResult.ToFailure(), false); + } + + return (null, true); + } + + public async Task<(Failure Failure, bool Data)> SaveChangeAsync(BindCellphoneRequest request, + CancellationToken cancel = default) + { + try + { + throw new DBConcurrencyException("insert data row concurrency error."); + } + catch (Exception e) + { + return (new Failure + { + Code = FailureCode.DataConcurrency, + Message = e.Message, + Exception = e, + Data = request + }, false); + } + } + + public async Task<(Failure Failure, bool Data)> ValidateCellphoneAsync(string cellphone, + CancellationToken cancel = default) + { + return (new Failure + { + Code = FailureCode.CellphoneFormatInvalid, + Message = "Cellphone format invalid.", + Data = cellphone + }, false); + } + + public async Task<(Failure Failure, GetMemberResult Data)> GetMemberAsync(int memberId, + CancellationToken cancel = default) + { + try + { + if (memberId == 1) + { + return (new Failure + { + Code = FailureCode.MemberNotFound, + Message = "member not found.", + }, null); + } + + throw new Exception($"can not connect db."); + } + catch (Exception e) + { + return (new Failure + { + Code = FailureCode.DbError, + Message = e.Message, + Data = memberId, + Exception = e, + }, null); + } + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService4.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService4.cs new file mode 100644 index 00000000..a7ba94ba --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberService4.cs @@ -0,0 +1,43 @@ +using FluentValidation; +using Lab.ErrorHandler.API.Extensions; +using Lab.ErrorHandler.API.Models; + +namespace Lab.ErrorHandler.API; + +public class MemberService4 +{ + private readonly IValidator _validator; + + public MemberService4(IValidator validator) + { + this._validator = validator; + } + + //一個方法有多種可能的 Failure + public async Task<(Failure Failure, bool Data)> BindCellphoneAsync(BindCellphoneRequest request, + CancellationToken cancel = default) + { + var executeResult = await this.ValidateModelAsync(request, cancel) + .GetMemberAsync(request.MemberId, cancel) + .ValidateCellphoneAsync(request.Cellphone, cancel) + .SaveChangeAsync(request, cancel); + if (executeResult.Failure != null) + { + return (executeResult.Failure, false); + } + + return (null, true); + } + + public async Task<(Failure Failure, bool Data)> ValidateModelAsync(BindCellphoneRequest request, + CancellationToken cancel = default) + { + var validationResult = await this._validator.ValidateAsync(request, cancel); + if (validationResult.IsValid == false) + { + return (validationResult.ToFailure(), false); + } + + return (null, true); + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberWorkflowExtensions.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberWorkflowExtensions.cs new file mode 100644 index 00000000..bb08c1af --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/MemberWorkflowExtensions.cs @@ -0,0 +1,80 @@ +using System.Data; +using Lab.ErrorHandler.API.Models; + +namespace Lab.ErrorHandler.API; + +static class MemberWorkflowExtensions +{ + public static async Task<(Failure Failure, GetMemberResult Data)> GetMemberAsync( + this Task<(Failure Failure, TSource Data)> previousStep, + int memberId, + CancellationToken cancel = default) + { + try + { + var previousStepResult = await previousStep; + if (previousStepResult.Failure != null) + { + return (previousStepResult.Failure, null); + } + + throw new Exception($"can not connect db."); + } + catch (Exception e) + { + return (new Failure + { + Code = FailureCode.DbError, + Message = e.Message, + Data = memberId, + Exception = e, + }, null); + } + } + + public static async Task<(Failure Failure, bool Data)> SaveChangeAsync( + this Task<(Failure Failure, TSource Data)> previousStep, + BindCellphoneRequest request, + CancellationToken cancel = default) + { + try + { + var previousStepResult = await previousStep; + if (previousStepResult.Failure != null) + { + return (previousStepResult.Failure, false); + } + + throw new DBConcurrencyException("insert data row concurrency error."); + } + catch (Exception e) + { + return (new Failure + { + Code = FailureCode.DataConcurrency, + Message = e.Message, + Exception = e, + Data = request + }, false); + } + } + + public static async Task<(Failure Failure, bool Data)> ValidateCellphoneAsync( + this Task<(Failure Failure, TSource Data)> previousStep, + string cellphone, + CancellationToken cancel = default) + { + var previousStepResult = await previousStep; + if (previousStepResult.Failure != null) + { + return (previousStepResult.Failure, false); + } + + return (new Failure + { + Code = FailureCode.CellphoneFormatInvalid, + Message = "Cellphone format invalid.", + Data = cellphone + }, false); + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/BindCellphoneRequest.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/BindCellphoneRequest.cs new file mode 100644 index 00000000..b1b58b22 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/BindCellphoneRequest.cs @@ -0,0 +1,8 @@ +namespace Lab.ErrorHandler.API.Models; + +public class BindCellphoneRequest +{ + public int MemberId { get; set; } + + public string Cellphone { get; set; } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/CreateMemberRequest.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/CreateMemberRequest.cs new file mode 100644 index 00000000..e30a3c75 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/CreateMemberRequest.cs @@ -0,0 +1,8 @@ +namespace Lab.ErrorHandler.API.Models; + +public class CreateMemberRequest +{ + public string Name { get; set; } + + public int Age { get; set; } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/Failure.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/Failure.cs new file mode 100644 index 00000000..7e5c71e5 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/Failure.cs @@ -0,0 +1,78 @@ +using System.Text.Json.Serialization; + +namespace Lab.ErrorHandler.API.Models; + +public class Failure +{ + public Failure() + { + } + + public Failure(FailureCode code, string message) + { + this.Code = code; + this.Message = message; + } + + /// + /// 錯誤碼 + /// + public FailureCode Code { get; init; } + + /// + /// 錯誤訊息 + /// + public string Message { get; init; } + + /// + /// 錯誤發生時的資料 + /// + public object Data { get; init; } + + /// + /// 追蹤 Id + /// + public string TraceId { get; set; } + + /// + /// 例外,不回傳給 Web API + /// + [JsonIgnore] + public Exception Exception { get; set; } + + public List Details { get; init; } = new(); + + //用了 [JsonIgnore] 似乎就不需要它了 QQ,寫完了才想到可以 Ignore,不過這仍然可以適用在其他場景,例如 CLI、Console App + public Failure WithoutException() + { + List details = new(); + foreach (var detail in this.Details) + { + details.Add(this.WithoutException(detail)); + } + + return new Failure(this.Code, this.Message) + { + Data = this.Data, + Details = details, + TraceId = this.TraceId, + }; + } + + public Failure WithoutException(Failure error) + { + var result = new Failure(error.Code, error.Message) + { + TraceId = error.TraceId + }; + + foreach (var detailError in this.Details) + { + // 遞迴處理 Details 屬性 + var detailResult = this.WithoutException(detailError); + result.Details.Add(detailResult); + } + + return result; + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/FailureCode.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/FailureCode.cs new file mode 100644 index 00000000..c9edbf84 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/FailureCode.cs @@ -0,0 +1,16 @@ +namespace Lab.ErrorHandler.API.Models; + +public enum FailureCode +{ + UnknownError = 0, + InputInvalid = 1, + MemberNotFound, + MemberAlreadyExist, + ServerError, + DataConflict, + DataConcurrency, + DataNotFound, + DbError, + S3Error, + CellphoneFormatInvalid +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/GenericResult.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/GenericResult.cs new file mode 100644 index 00000000..7d220681 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/GenericResult.cs @@ -0,0 +1,8 @@ +namespace Lab.ErrorHandler.API.Models; + +public class GenericResult +{ + public Failure Failure { get; set; } + + public T Data { get; set; } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/GetMemberResult.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/GetMemberResult.cs new file mode 100644 index 00000000..841fe50d --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Models/GetMemberResult.cs @@ -0,0 +1,12 @@ +namespace Lab.ErrorHandler.API.Models; + +public class GetMemberResult +{ + public int Id { get; set; } + + public string Name { get; set; } + + public int Age { get; set; } + + public string Cellphone { get; set; } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Program.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Program.cs new file mode 100644 index 00000000..9c6497f6 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Program.cs @@ -0,0 +1,73 @@ +using System.Diagnostics; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using FluentValidation; +using Lab.ErrorHandler.API; +using Lab.ErrorHandler.API.Filters; +using Lab.ErrorHandler.API.Models; +using Microsoft.AspNetCore.Mvc; +using Serilog; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services + .AddControllers(p => + { + // p.Filters.Add(); + + // p.ModelValidatorProviders.Clear(); + }) + .AddJsonOptions(options => + { + options.JsonSerializerOptions.MaxDepth = 10; + options.JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + options.AllowInputFormatterExceptionMessages = true; + }) + ; +builder.Services.Configure(options => +{ + //停用 Model State Invalid Filter + options.SuppressModelStateInvalidFilter = true; +}); + +builder.Services.AddValidatorsFromAssemblyContaining(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Host.UseSerilog((context, services, config) => + config.ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/aspnet-.txt", rollingInterval: RollingInterval.Hour) +); + +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/Error Handler/Without Exception/Lab.ErrorHandler.API/Properties/launchSettings.json b/Error Handler/Without Exception/Lab.ErrorHandler.API/Properties/launchSettings.json new file mode 100644 index 00000000..e88fda27 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:19210", + "sslPort": 44321 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5031", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7164;http://localhost:5031", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/SecondStepExtensions.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/SecondStepExtensions.cs new file mode 100644 index 00000000..44160860 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/SecondStepExtensions.cs @@ -0,0 +1,41 @@ +using Lab.ErrorHandler.API.Models; + +namespace Lab.ErrorHandler.API; + +public static class SecondStepExtensions +{ + /// + /// 接續執行第二個方法 + /// + /// + /// + /// + /// + /// + public static async Task Then(this Task first, + Func> second) + { + return await second(await first.ConfigureAwait(false)).ConfigureAwait(false); + } + + /// + /// 接續第二個方法,第一個方法有錯誤時,不執行第二個方法 + /// + /// + /// + /// + /// + /// + public static async Task<(Failure Failure, TResult Data)> WhenSuccess( + this Task<(Failure Failure, TSource Data)> first, + Func> second) + { + var result = await first.ConfigureAwait(false); + if (result.Failure != null) + { + return (result.Failure, default(TResult)); + } + + return await second(result.Data).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Validators/BindCellphoneRequestValidator.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Validators/BindCellphoneRequestValidator.cs new file mode 100644 index 00000000..50a0c159 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Validators/BindCellphoneRequestValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using Lab.ErrorHandler.API.Models; + +namespace Lab.ErrorHandler.API.Validators; + +public class BindCellphoneRequestValidator : AbstractValidator +{ + public BindCellphoneRequestValidator() + { + this.RuleFor(p => p.Cellphone).NotNull().NotEmpty(); + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/Validators/CreateMemberRequestValidator.cs b/Error Handler/Without Exception/Lab.ErrorHandler.API/Validators/CreateMemberRequestValidator.cs new file mode 100644 index 00000000..4bf1b89a --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/Validators/CreateMemberRequestValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using Lab.ErrorHandler.API.Models; + +namespace Lab.ErrorHandler.API.Validators; + +public class CreateMemberRequestValidator : AbstractValidator +{ + public CreateMemberRequestValidator() + { + this.RuleFor(p => p.Name).NotNull().NotEmpty(); + this.RuleFor(p => p.Age).LessThanOrEqualTo(18).GreaterThanOrEqualTo(200); + } +} \ No newline at end of file diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/appsettings.Development.json b/Error Handler/Without Exception/Lab.ErrorHandler.API/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Error Handler/Without Exception/Lab.ErrorHandler.API/appsettings.json b/Error Handler/Without Exception/Lab.ErrorHandler.API/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Error Handler/Without Exception/Lab.ErrorHandler.API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Event Bus/MassTransit/Lab.MassTransit/Consumer/Consumer.csproj b/Event Bus/MassTransit/Lab.MassTransit/Consumer/Consumer.csproj new file mode 100644 index 00000000..e5e56586 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Consumer/Consumer.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/Event Bus/MassTransit/Lab.MassTransit/Consumer/OrderSubmittedConsumer.cs b/Event Bus/MassTransit/Lab.MassTransit/Consumer/OrderSubmittedConsumer.cs new file mode 100644 index 00000000..df92b5ac --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Consumer/OrderSubmittedConsumer.cs @@ -0,0 +1,24 @@ +using MassTransit; +using Message; + +namespace Consumer; + +public class OrderSubmittedConsumer : IConsumer +{ + public async Task Consume(ConsumeContext context) + { + // var destinationAddress = new Uri("rabbitmq://localhost/order-submitted-queue"); + // var command = new OrderSubmitted() + // { + // OrderId = context.Message.OrderId, + // Timestamp = context.Message.Timestamp + // }; + // + // await context.Send(destinationAddress, command); + + // var endpoint = await context.GetSendEndpoint(destinationAddress); + // await endpoint.Send(command); + + Console.WriteLine($"Order received: {context.Message.OrderId} at {context.Message.Timestamp}"); + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Consumer/Program.cs b/Event Bus/MassTransit/Lab.MassTransit/Consumer/Program.cs new file mode 100644 index 00000000..de0ecfe7 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Consumer/Program.cs @@ -0,0 +1,45 @@ +using Consumer; +using MassTransit; +using Microsoft.Extensions.Hosting; + +public class Program +{ + public static async Task Main(string[] args) + { + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddMassTransit(x => + { + x.AddConsumer(); + + x.UsingRabbitMq((context, config) => + { + config.Host("rabbitmq://localhost", h => + { + h.Username("guest"); + h.Password("guest"); + }); + + // 設置接收端點,並消費 `OrderSubmitted` + config.ReceiveEndpoint("order-submitted-queue", endpoint => + { + endpoint.ConfigureConsumer(context); + }); + }); + }); + + services.AddMassTransitHostedService(); + }) + .Build(); + try + { + Console.WriteLine("Listening for OrderSubmitted events..."); + await host.RunAsync(); + } + finally + { + await host.StopAsync(); + } + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Consumer2/Consumer2.csproj b/Event Bus/MassTransit/Lab.MassTransit/Consumer2/Consumer2.csproj new file mode 100644 index 00000000..4a84631b --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Consumer2/Consumer2.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Event Bus/MassTransit/Lab.MassTransit/Consumer2/OrderSubmittedConsumer.cs b/Event Bus/MassTransit/Lab.MassTransit/Consumer2/OrderSubmittedConsumer.cs new file mode 100644 index 00000000..883212ae --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Consumer2/OrderSubmittedConsumer.cs @@ -0,0 +1,23 @@ +using MassTransit; +using Message; + +namespace Consumer2; + +public class OrderSubmittedConsumer : IConsumer +{ + public async Task Consume(ConsumeContext context) + { + var destinationAddress = new Uri("rabbitmq://localhost/order-submitted-queue"); + var command = new OrderSubmitted() + { + OrderId = context.Message.OrderId, + Timestamp = context.Message.Timestamp + }; + + await context.Send(destinationAddress, command); + + // var endpoint = await context.GetSendEndpoint(destinationAddress); + // await endpoint.Send(command); + Console.WriteLine($"Order received: {context.Message.OrderId} at {context.Message.Timestamp}"); + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Consumer2/Program.cs b/Event Bus/MassTransit/Lab.MassTransit/Consumer2/Program.cs new file mode 100644 index 00000000..c38ebe2b --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Consumer2/Program.cs @@ -0,0 +1,46 @@ +using MassTransit; +using Microsoft.Extensions.Hosting; + +namespace Consumer2; + +public class Program +{ + public static async Task Main(string[] args) + { + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((_, services) => + { + services.AddMassTransit(x => + { + x.AddConsumer(); + + x.UsingRabbitMq((context, config) => + { + config.Host("rabbitmq://localhost", h => + { + h.Username("guest"); + h.Password("guest"); + }); + + // 設置接收端點,並消費 `OrderSubmitted` + config.ReceiveEndpoint("order-submitted-queue2", endpoint => + { + endpoint.ConfigureConsumer(context); + }); + }); + }); + + services.AddMassTransitHostedService(); + }) + .Build(); + try + { + Console.WriteLine("Listening for OrderSubmitted events..."); + await host.RunAsync(); + } + finally + { + await host.StopAsync(); + } + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Lab.MassTransit.WebAPI.csproj b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Lab.MassTransit.WebAPI.csproj new file mode 100644 index 00000000..72c5d824 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Lab.MassTransit.WebAPI.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + Linux + + + + + + + + + + + diff --git a/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Lab.MassTransit.WebAPI.http b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Lab.MassTransit.WebAPI.http new file mode 100644 index 00000000..6866c1e4 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Lab.MassTransit.WebAPI.http @@ -0,0 +1,6 @@ +@Lab.MassTransit.WebAPI_HostAddress = http://localhost:5089 + +GET {{Lab.MassTransit.WebAPI_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/CreateOrderRequest.cs b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/CreateOrderRequest.cs new file mode 100644 index 00000000..be2aea87 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/CreateOrderRequest.cs @@ -0,0 +1,7 @@ +namespace Lab.MassTransit.WebAPI.Order; + +// 訂單請求 +public class CreateOrderRequest +{ + public decimal TotalAmount { get; set; } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/OrderCreated.cs b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/OrderCreated.cs new file mode 100644 index 00000000..02e8175e --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/OrderCreated.cs @@ -0,0 +1,11 @@ +namespace Lab.MassTransit.WebAPI.Order; + +// 訂單已建立 +public class OrderCreated +{ + public Guid OrderId { get; set; } + + public DateTime CreatedAt { get; set; } + + public decimal TotalAmount { get; set; } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/OrderCreatedConsumer.cs b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/OrderCreatedConsumer.cs new file mode 100644 index 00000000..1af54d6e --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/OrderCreatedConsumer.cs @@ -0,0 +1,12 @@ +using MassTransit; + +namespace Lab.MassTransit.WebAPI.Order; + +public class OrderCreatedConsumer : IConsumer +{ + public Task Consume(ConsumeContext context) + { + Console.WriteLine($"Order created: {context.Message.OrderId}, Total Amount: {context.Message.TotalAmount}"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/OrdersController.cs b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/OrdersController.cs new file mode 100644 index 00000000..bbd71fbd --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Order/OrdersController.cs @@ -0,0 +1,36 @@ +using MassTransit; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.MassTransit.WebAPI.Order; + +[ApiController] +[Route("api/[controller]")] +public class OrdersController : ControllerBase +{ + private readonly IPublishEndpoint _publishEndpoint; + public OrdersController(IPublishEndpoint publishEndpoint) + { + this._publishEndpoint = publishEndpoint; + } + + [HttpPost] + public async Task CreateOrder([FromBody] CreateOrderRequest request) + { + if (request == null) + { + return this.BadRequest("Invalid order data"); + } + + var orderCreatedEvent = new OrderCreated + { + OrderId = Guid.NewGuid(), + CreatedAt = DateTime.UtcNow, + TotalAmount = request.TotalAmount + }; + + // 生產者,發布 OrderCreated 事件 + await this._publishEndpoint.Publish(orderCreatedEvent); + + return this.Ok($"Order created with ID: {orderCreatedEvent.OrderId}"); + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Program.cs b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Program.cs new file mode 100644 index 00000000..0c3cb1d8 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Program.cs @@ -0,0 +1,48 @@ +using Lab.MassTransit.WebAPI; +using Lab.MassTransit.WebAPI.Order; +using MassTransit; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddMassTransit(x => +{ + // 註冊消費者 + x.AddConsumer(); + + // 配置 MassTransit 使用 RabbitMQ + x.UsingRabbitMq((context, config) => + { + config.Host("localhost", "/", h => + { + h.Username("guest"); + h.Password("guest"); + }); + + // 註冊消費者 + config.ReceiveEndpoint("order-created-event", e => + { + e.ConfigureConsumer(context); + }); + }); +}); +builder.Services.AddControllers(); +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +// 使用中介軟體和路由 +app.UseRouting(); +app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + +app.UseHttpsRedirection(); + +app.Run(); \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Properties/launchSettings.json b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Properties/launchSettings.json new file mode 100644 index 00000000..939c798a --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:65484", + "sslPort": 44345 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5089", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7293;http://localhost:5089", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/appsettings.Development.json b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/appsettings.json b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.WebAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.sln b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.sln new file mode 100644 index 00000000..cce82d28 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Lab.MassTransit.sln @@ -0,0 +1,45 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.MassTransit.WebAPI", "Lab.MassTransit.WebAPI\Lab.MassTransit.WebAPI.csproj", "{626813B6-52DA-479B-A50B-94E364E63F3C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{34BE7295-45E2-45CE-997C-3A9F533583A5}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Consumer", "Consumer\Consumer.csproj", "{EB047813-167A-45E5-BFE3-BC11DBECFC25}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Producer", "Producer\Producer.csproj", "{8D064162-1E9B-4D63-A6F5-4A528B6FECC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Message", "Message\Message.csproj", "{22B7AE06-65AE-4234-9484-11A6FF7AD413}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Consumer2", "Consumer2\Consumer2.csproj", "{29ABECC5-822C-4929-AE85-F302FEFDF9C9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {626813B6-52DA-479B-A50B-94E364E63F3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {626813B6-52DA-479B-A50B-94E364E63F3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {626813B6-52DA-479B-A50B-94E364E63F3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {626813B6-52DA-479B-A50B-94E364E63F3C}.Release|Any CPU.Build.0 = Release|Any CPU + {EB047813-167A-45E5-BFE3-BC11DBECFC25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB047813-167A-45E5-BFE3-BC11DBECFC25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB047813-167A-45E5-BFE3-BC11DBECFC25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB047813-167A-45E5-BFE3-BC11DBECFC25}.Release|Any CPU.Build.0 = Release|Any CPU + {8D064162-1E9B-4D63-A6F5-4A528B6FECC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D064162-1E9B-4D63-A6F5-4A528B6FECC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D064162-1E9B-4D63-A6F5-4A528B6FECC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D064162-1E9B-4D63-A6F5-4A528B6FECC5}.Release|Any CPU.Build.0 = Release|Any CPU + {22B7AE06-65AE-4234-9484-11A6FF7AD413}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22B7AE06-65AE-4234-9484-11A6FF7AD413}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22B7AE06-65AE-4234-9484-11A6FF7AD413}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22B7AE06-65AE-4234-9484-11A6FF7AD413}.Release|Any CPU.Build.0 = Release|Any CPU + {29ABECC5-822C-4929-AE85-F302FEFDF9C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29ABECC5-822C-4929-AE85-F302FEFDF9C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29ABECC5-822C-4929-AE85-F302FEFDF9C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29ABECC5-822C-4929-AE85-F302FEFDF9C9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Event Bus/MassTransit/Lab.MassTransit/Message/Message.csproj b/Event Bus/MassTransit/Lab.MassTransit/Message/Message.csproj new file mode 100644 index 00000000..3a635329 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Message/Message.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Event Bus/MassTransit/Lab.MassTransit/Message/OrderSubmitted.cs b/Event Bus/MassTransit/Lab.MassTransit/Message/OrderSubmitted.cs new file mode 100644 index 00000000..251cb956 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Message/OrderSubmitted.cs @@ -0,0 +1,7 @@ +namespace Message; + +public class OrderSubmitted +{ + public Guid OrderId { get; set; } + public DateTime Timestamp { get; set; } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService.cs b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService.cs new file mode 100644 index 00000000..4fc36946 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService.cs @@ -0,0 +1,30 @@ +using MassTransit; +using Message; +using Microsoft.Extensions.Hosting; + +public class MessagePublishService : IHostedService +{ + private readonly IPublishEndpoint _publishEndpoint; + + public MessagePublishService(IPublishEndpoint publishEndpoint) + { + this._publishEndpoint = publishEndpoint; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + // 使用 Publish 發佈事件,所有訂閱者都能接收此事件 + await this._publishEndpoint.Publish(new OrderSubmitted + { + OrderId = Guid.NewGuid(), + Timestamp = DateTime.UtcNow + }, cancellationToken); + + Console.WriteLine("OrderSubmitted event published."); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService2.cs b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService2.cs new file mode 100644 index 00000000..9f73bc8e --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService2.cs @@ -0,0 +1,38 @@ +using MassTransit; +using Message; +using Microsoft.Extensions.Hosting; + +public class MessagePublishService2 : IHostedService +{ + private readonly IBus _bus; + + public MessagePublishService2(IBus bus) + { + this._bus = bus; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var orderSubmitted = new OrderSubmitted + { + OrderId = Guid.NewGuid(), + Timestamp = DateTime.UtcNow + }; + // ===Publish=== + // 使用 Publish 發佈事件,所有訂閱者都能接收此事件 + await this._bus.Publish(orderSubmitted, cancellationToken); + Console.WriteLine("OrderSubmitted event published."); + + // ===Send=== + // EndpointConvention.Map(new Uri("queue:order-submitted-queue")); + EndpointConvention.Map(new Uri("rabbitmq://localhost/order-submitted-queue")); + + await this._bus.Send(orderSubmitted, cancellationToken); + Console.WriteLine("OrderSubmitted event sent."); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService3.cs b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService3.cs new file mode 100644 index 00000000..5595fe6d --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService3.cs @@ -0,0 +1,34 @@ +using MassTransit; +using Message; +using Microsoft.Extensions.Hosting; + +public class MessagePublishService3 : IHostedService +{ + private readonly ISendEndpointProvider _sendEndpointProvider; + + public MessagePublishService3(ISendEndpointProvider sendEndpointProvider) + { + this._sendEndpointProvider = sendEndpointProvider; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var orderSubmitted = new OrderSubmitted + { + OrderId = Guid.NewGuid(), + Timestamp = DateTime.UtcNow + }; + + // EndpointConvention.Map(new Uri("queue:order-submitted-queue")); + // var uri = new Uri("rabbitmq://localhost/order-submitted-queue"); + var uri = new Uri("queue:order-submitted-queue"); + var endpoint = await this._sendEndpointProvider.GetSendEndpoint(uri); + await endpoint.Send(orderSubmitted, cancellationToken); + Console.WriteLine("OrderSubmitted event sent."); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService4.cs b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService4.cs new file mode 100644 index 00000000..374c7d1a --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessagePublishService4.cs @@ -0,0 +1,55 @@ +using MassTransit; +using Message; +using Microsoft.Extensions.Hosting; + +public class MessagePublishService4 : IHostedService +{ + + private readonly IBusControl busControl; + + public MessagePublishService4(IBusControl bus) + { + this.busControl = bus; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + // var busControl = Bus.Factory.CreateUsingRabbitMq(config => + // { + // config.Host("rabbitmq://localhost", h => + // { + // h.Username("guest"); + // h.Password("guest"); + // }); + // }); + await busControl.StartAsync(cancellationToken); + + var orderSubmitted = new OrderSubmitted + { + OrderId = Guid.NewGuid(), + Timestamp = DateTime.UtcNow + }; + + // 發佈事件 + await busControl.Publish(new OrderSubmitted + { + OrderId = Guid.NewGuid(), + Timestamp = DateTime.UtcNow + }, cancellationToken); + + // ===Publish=== + // 使用 Publish 發佈事件,所有訂閱者都能接收此事件 + await busControl.Publish(orderSubmitted, cancellationToken); + Console.WriteLine("OrderSubmitted event published."); + + // ===Send=== + EndpointConvention.Map(new Uri("rabbitmq://localhost/order-submitted-queue")); + await busControl.Send(orderSubmitted, cancellationToken); + Console.WriteLine("OrderSubmitted event sent."); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Producer/MessageSenderService.cs b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessageSenderService.cs new file mode 100644 index 00000000..44ebd01f --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessageSenderService.cs @@ -0,0 +1,37 @@ +using MassTransit; +using Message; +using Microsoft.Extensions.Hosting; + +namespace Producer; + +public class MessageSenderService : IHostedService +{ + private readonly ISendEndpointProvider _sendEndpointProvider; + + public MessageSenderService(ISendEndpointProvider sendEndpointProvider) + { + this._sendEndpointProvider = sendEndpointProvider; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + // 取得 SendEndpoint 並發送訊息到指定佇列 + var sendEndpoint = + + // await this._sendEndpointProvider.GetSendEndpoint(new Uri("queue:order-submitted-queue")); + await this._sendEndpointProvider.GetSendEndpoint(new Uri("rabbitmq://localhost/order-submitted-queue")); + + await sendEndpoint.Send(new OrderSubmitted + { + OrderId = Guid.NewGuid(), + Timestamp = DateTime.UtcNow + }, cancellationToken); + + Console.WriteLine("OrderSubmitted event sent to order-submitted-queue."); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Producer/MessageSenderService2.cs b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessageSenderService2.cs new file mode 100644 index 00000000..2689076e --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Producer/MessageSenderService2.cs @@ -0,0 +1,39 @@ +using MassTransit; +using Message; +using Microsoft.Extensions.Hosting; + +public class MessageSenderService2 : IHostedService +{ + private readonly IBus _bus; + + public MessageSenderService2(IBus bus) + { + this._bus = bus; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var orderSubmitted = new OrderSubmitted + { + OrderId = Guid.NewGuid(), + Timestamp = DateTime.UtcNow + }; + + // ===Publish=== + // 使用 Publish 發佈事件,所有訂閱者都能接收此事件 + // await this._bus.Publish(orderSubmitted, cancellationToken); + // Console.WriteLine("OrderSubmitted event published."); + + // ===Send=== + // EndpointConvention.Map(new Uri("queue:order-submitted-queue")); + EndpointConvention.Map(new Uri("rabbitmq://localhost/order-submitted-queue")); + + await this._bus.Send(orderSubmitted, cancellationToken); + Console.WriteLine("OrderSubmitted event sent."); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Producer/Producer.csproj b/Event Bus/MassTransit/Lab.MassTransit/Producer/Producer.csproj new file mode 100644 index 00000000..e5e56586 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Producer/Producer.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/Event Bus/MassTransit/Lab.MassTransit/Producer/Program.cs b/Event Bus/MassTransit/Lab.MassTransit/Producer/Program.cs new file mode 100644 index 00000000..3796e260 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Producer/Program.cs @@ -0,0 +1,36 @@ +using MassTransit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Producer; + +public class Program +{ + public static async Task Main(string[] args) + { + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddMassTransit(x => + { + // 配置 MassTransit 使用 RabbitMQ + x.UsingRabbitMq((context, config) => + { + config.Host("localhost", "/", h => + { + h.Username("guest"); + h.Password("guest"); + }); + }); + }); + + // 將 MessageSenderService 註冊為 IHostedService + // services.AddHostedService(); + services.AddHostedService(); + // services.AddHostedService(); + }) + .Build(); + + await host.StartAsync(); + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/Producer/Program.cs1 b/Event Bus/MassTransit/Lab.MassTransit/Producer/Program.cs1 new file mode 100644 index 00000000..9b1309e8 --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/Producer/Program.cs1 @@ -0,0 +1,38 @@ +using MassTransit; +using Message; + +namespace Producer; + +public class Program +{ + public static async Task Main(string[] args) + { + var busControl = Bus.Factory.CreateUsingRabbitMq(config => + { + config.Host("rabbitmq://localhost", h => + { + h.Username("guest"); + h.Password("guest"); + }); + + }); + + var sendEndpoint = await busControl.GetSendEndpoint(new Uri("rabbitmq://localhost/order-submitted-queue")); + + try + { + // 發佈事件 + await sendEndpoint.Send(new OrderSubmitted + { + OrderId = Guid.NewGuid(), + Timestamp = DateTime.UtcNow + }); + + Console.WriteLine("OrderSubmitted event published."); + } + finally + { + await busControl.StopAsync(); + } + } +} \ No newline at end of file diff --git a/Event Bus/MassTransit/Lab.MassTransit/docker-compose.yml b/Event Bus/MassTransit/Lab.MassTransit/docker-compose.yml new file mode 100644 index 00000000..2fdef00c --- /dev/null +++ b/Event Bus/MassTransit/Lab.MassTransit/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3.8' +services: + rabbitmq: + container_name: rabbitmq.3 + image: "rabbitmq:3-management" + ports: + - "5672:5672" # RabbitMQ 主要連接埠 + - "15672:15672" # 管理介面連接埠 + environment: + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest diff --git "a/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/10\350\220\254.xlsx" "b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/10\350\220\254.xlsx" new file mode 100644 index 00000000..a8330222 Binary files /dev/null and "b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/10\350\220\254.xlsx" differ diff --git a/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/GlobalUsings.cs b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/GlobalUsings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Import.xlsx b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Import.xlsx new file mode 100644 index 00000000..20db008d Binary files /dev/null and b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Import.xlsx differ diff --git a/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Lab.MiniExcelQuery.Test.csproj b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Lab.MiniExcelQuery.Test.csproj new file mode 100644 index 00000000..29fa4291 --- /dev/null +++ b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Lab.MiniExcelQuery.Test.csproj @@ -0,0 +1,56 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Template/Employee.xlsx b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Template/Employee.xlsx new file mode 100644 index 00000000..0114a247 Binary files /dev/null and b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Template/Employee.xlsx differ diff --git a/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Template/Member.xlsx b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Template/Member.xlsx new file mode 100644 index 00000000..c71d8879 Binary files /dev/null and b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Template/Member.xlsx differ diff --git a/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Template/Template.xltx b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Template/Template.xltx new file mode 100644 index 00000000..ac798dae Binary files /dev/null and b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/Template/Template.xltx differ diff --git a/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/UnitTest1.cs b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/UnitTest1.cs new file mode 100644 index 00000000..7a228f6f --- /dev/null +++ b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.Test/UnitTest1.cs @@ -0,0 +1,281 @@ +using System.ComponentModel; +using MiniExcelLibs; +using MiniExcelLibs.Attributes; +using MiniExcelLibs.OpenXml; + +namespace Lab.MiniExcelQuery.Test; + +public class UnitTest1 +{ + class Member + { + [ExcelColumnName(excelColumnName: "編號")] + public string Id { get; set; } + + [ExcelColumnName(excelColumnName: "姓名")] + public string Name { get; set; } + + [ExcelColumnName(excelColumnName: "生日")] + public DateTime Birthday { get; set; } + + // [ExcelColumnName(excelColumnName: "年齡", aliases: ["Age"])] + [DisplayName("年齡")] + public int Age { get; set; } + + [DisplayName("電話")] + + public string Phone { get; set; } + + [DisplayName("失敗原因")] + public string Reason { get; set; } + } + + class Data + { + [ExcelColumnIndex("A")] + [ExcelColumnName("商品名稱")] + public string SaleName { get; set; } + + [ExcelColumnIndex("B")] + [ExcelColumnName("規格")] + public string Option { get; set; } + + [ExcelColumnIndex("C")] + [ExcelColumnName("編號")] + public string Id { get; set; } + + [ExcelColumnIndex("D")] + [ExcelColumnName("Code")] + public string Code { get; set; } + + [ExcelColumnIndex("E")] + [ExcelColumnName("庫存")] + public string StockQty { get; set; } + } + + [Fact] + public async Task 批次匯入大檔案() + { + //import large excel file + var inputPath = "10萬.xlsx"; + var chunkSize = 1024; + + await using var inputStream = File.OpenRead(inputPath); + var results = new List(); + + // chunk read the data + // var inputRows = await MiniExcel.QueryAsync(inputPath); + var inputRows = await inputStream.QueryAsync(); + foreach (var chunks in inputRows.Chunk(chunkSize)) + { + results.AddRange(chunks); + } + + // var lookup = new List>(); + // foreach (IDictionary row in await MiniExcel.QueryAsync(inputPath)) + // { + // lookup.Add(row); + // } + } + + [Fact] + public async Task 批次匯入後批次填充範本() + { + //import excel file + var inputPath = "Import.xlsx"; + var outputPath = "MemberResult.xlsx"; + var templatePath = "Template/Member.xlsx"; + var chunkSize = 2; + + await using var inputStream = File.OpenRead(inputPath); + var inputRows = await inputStream.QueryAsync(); + var results = new List(); + foreach (var chunks in inputRows.Chunk(chunkSize)) + { + foreach (var row in chunks) + { + var age = (DateTime.Now - row.Birthday).TotalDays / 365.25; + if (age > 30) + { + row.Reason = "年齡超過30"; + } + + row.Age = (int)age; + } + + results.AddRange(chunks); + var value = new + { + Members = results + }; + + //Append to the same file + await MiniExcel.SaveAsByTemplateAsync(outputPath, templatePath, value); + } + } + + [Fact] + public async Task 匿名型別填充員工表() + { + var templatePath = "Template/Employee.xlsx"; + + // var templatePath = "Template/ImportWithError.xltx"; + var outputPath = "EmployeeResult.xlsx"; + var value = new + { + employees = new[] + { + new { name = "Jack", department = "HR" }, + new { name = "Lisa", department = "HR" }, + new { name = "John", department = "HR" }, + new { name = "Mike", department = "IT" }, + new { name = "Neo", department = "IT" }, + new { name = "Loan", department = "IT" } + } + }; + await MiniExcel.SaveAsByTemplateAsync(outputPath, templatePath, value); + } + + [Fact] + public async Task 強型別填充會員表() + { + var templatePath = "Template/Member.xlsx"; + + // var templatePath = "Template/ImportWithError.xltx"; + var outputPath = "MemberResult.xlsx"; + + var value = new + { + Members = new List() + { + new() + { + Id = "1", + Name = "Alice", + Age = 25, + Phone = "1234567890", + Reason = null, + }, + new() + { + Id = "2", + Name = "Bob", + Age = 35, + Phone = "1234567890", + Reason = "年齡超過30", + } + } + }; + await MiniExcel.SaveAsByTemplateAsync(outputPath, templatePath, value); + } + + [Fact] + public async Task 強型別另存會員表() + { + var outputPath = "MemberResult.xlsx"; + + await using var outputStream = File.Open(outputPath, FileMode.OpenOrCreate, FileAccess.ReadWrite); + var value = new List() + { + new() + { + Id = "1", + Name = "Alice", + Birthday = DateTime.Now, + Age = 25, + Phone = "1234567890", + Reason = null, + }, + new() + { + Id = "2", + Name = "Bob", + Birthday = DateTime.Now, + Age = 35, + Phone = "1234567890", + Reason = "年齡超過30", + } + }; + + // await MiniExcel.SaveAsAsync(outputPath, value, overwriteFile: true); + await outputStream.SaveAsAsync(value); + } + + [Fact] + public async Task 產生大資料後填充範本() + { + //import excel file + var outputPath = "MemberResult.xlsx"; + var templatePath = "Template/Member.xlsx"; + var chunkSize = 128; + + //generate 10000000 member row + var inputRows = Enumerable.Range(1, 800000).Select(x => new Member + { + Id = x.ToString(), + Name = "Name" + x, + Birthday = DateTime.Now, + Phone = "1234567890" + }); + var results = new List(); + foreach (var chunks in inputRows.Chunk(chunkSize)) + { + foreach (var row in chunks) + { + var age = (DateTime.Now - row.Birthday).TotalDays / 365.25; + if (age > 30) + { + row.Reason = "年齡超過30"; + } + + row.Age = (int)age; + } + + results.AddRange(chunks); + } + + var value = new + { + Members = results + }; + + //Append to the same file + MiniExcel.SaveAsByTemplate(outputPath, templatePath, value); + } + + [Fact] + public async Task 產生大資料後匯出() + { + //import excel file + var outputPath = "MemberResult.xlsx"; + var chunkSize = 128; + + //generate 10000000 member row + var inputRows = Enumerable.Range(1, 800000).Select(x => new Member + { + Id = x.ToString(), + Name = "Name" + x, + Birthday = DateTime.Now, + Phone = "1234567890" + }); + var results = new List(); + foreach (var chunks in inputRows.Chunk(chunkSize)) + { + foreach (var row in chunks) + { + var age = (DateTime.Now - row.Birthday).TotalDays / 365.25; + if (age > 30) + { + row.Reason = "年齡超過30"; + } + + row.Age = (int)age; + } + + results.AddRange(chunks); + } + + //Append to the same file + MiniExcel.SaveAs(outputPath, results, overwriteFile: true); + } +} \ No newline at end of file diff --git a/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.sln b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.sln new file mode 100644 index 00000000..748c7985 --- /dev/null +++ b/Excel/Lab.MiniExcelQuery/Lab.MiniExcelQuery.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.MiniExcelQuery.Test", "Lab.MiniExcelQuery.Test\Lab.MiniExcelQuery.Test.csproj", "{854528C7-3938-400B-B18A-4E3438BAB2EC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {854528C7-3938-400B-B18A-4E3438BAB2EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {854528C7-3938-400B-B18A-4E3438BAB2EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {854528C7-3938-400B-B18A-4E3438BAB2EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {854528C7-3938-400B-B18A-4E3438BAB2EC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Excel/Mapper/Lab.NpoiMapper/NETCore31/Employee.cs b/Excel/Mapper/Lab.NpoiMapper/NETCore31/Employee.cs index cca46ade..1e0c2170 100644 --- a/Excel/Mapper/Lab.NpoiMapper/NETCore31/Employee.cs +++ b/Excel/Mapper/Lab.NpoiMapper/NETCore31/Employee.cs @@ -10,7 +10,7 @@ internal class Employee public string LocationId { get; set; } //[Column("DeptID")] - [Display(Name = "DeptID")] + [Display(Name = "DeptID")]//對應Excel欄位 public string DepartmentId { get; set; } [Column("DeptName")] diff --git a/Excel/Mapper/Lab.NpoiMapper/NETCore31/NETCore31.csproj b/Excel/Mapper/Lab.NpoiMapper/NETCore31/NETCore31.csproj index 8a2b404a..5a4269c1 100644 --- a/Excel/Mapper/Lab.NpoiMapper/NETCore31/NETCore31.csproj +++ b/Excel/Mapper/Lab.NpoiMapper/NETCore31/NETCore31.csproj @@ -19,6 +19,9 @@ Always + + Always + diff --git a/Excel/Mapper/Lab.NpoiMapper/NETCore31/UnitTest1.cs b/Excel/Mapper/Lab.NpoiMapper/NETCore31/UnitTest1.cs index 44401f41..b99ce3a2 100644 --- a/Excel/Mapper/Lab.NpoiMapper/NETCore31/UnitTest1.cs +++ b/Excel/Mapper/Lab.NpoiMapper/NETCore31/UnitTest1.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; @@ -136,5 +137,46 @@ public class UnitTest1 Console.WriteLine(JsonConvert.SerializeObject(rowData)); } } + + [TestMethod] + public void Ūdɫtss() + { + var inputStream = File.Open("Template.xlsx", FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + // var mapper = new Mapper("Template.xlsx"); + var mapper = new Mapper(inputStream); + var employees = new List + { + new Employee + { + Id = 1, + LocationId = "A", + DepartmentId = "S000", + DepartmentName = "si", + EmployeeId = "S001", + Name = "Ep", + DomainName = "TEST", + Birthdaty = new DateTime(1988, 9, 11), + ErrorMessage = null + }, + new Employee + { + Id = 2, + LocationId = "A", + DepartmentId = "A000", + DepartmentName = "", + EmployeeId = "A001", + Name = "p", + DomainName = "TEST", + Birthdaty = new DateTime(1976, 8, 22), + ErrorMessage = "ڿF" + }, + }; + + mapper.Put(employees, overwrite: true); + // mapper.Save("Output.xlsx"); + var outputStream = File.Open("Output.xlsx", FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); + mapper.Save(outputStream); + } } } \ No newline at end of file diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/DemoUnitTest.cs b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/DemoUnitTest.cs new file mode 100644 index 00000000..590d1e06 --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/DemoUnitTest.cs @@ -0,0 +1,40 @@ +using Lab.FeatureToggle.WebAPI; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.FeatureManagement; + +namespace Lab.FeatureToggle.TestProject; + +[TestClass] +public class DemoUnitTest +{ + [TestMethod] + public async Task CreateFeatureA() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddFeatureManagement(configBuilder.Build()); + var serviceProvider = services.BuildServiceProvider(); + var target = serviceProvider.GetService(); + var actual = await target.CreateFeatureA(); + Assert.AreEqual("OK", actual); + } + + [TestMethod] + public async Task CreateFeatureB() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddFeatureManagement(configBuilder.Build()); + var serviceProvider = services.BuildServiceProvider(); + var target = serviceProvider.GetService(); + var actual = await target.CreateFeatureB(); + Assert.AreEqual(null, actual); + } +} \ No newline at end of file diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/Lab.FeatureToggle.TestProject.csproj b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/Lab.FeatureToggle.TestProject.csproj new file mode 100644 index 00000000..a870d683 --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/Lab.FeatureToggle.TestProject.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + + + + + true + PreserveNewest + PreserveNewest + + + + + + + diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/Usings.cs b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/appsettings.json b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/appsettings.json new file mode 100644 index 00000000..0b7b878a --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.TestProject/appsettings.json @@ -0,0 +1,23 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "FeatureManagement": { + "FeatureA": true, + "FeatureB": false, + "FeatureC": { + "EnabledFor": [ + { + "Name": "Percentage", + "Parameters": { + "Value": 50 + } + } + ] + } + }, + "AllowedHosts": "*" +} diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Controllers/DemoController.cs b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Controllers/DemoController.cs new file mode 100644 index 00000000..e402ea34 --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Controllers/DemoController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.FeatureManagement; +using Microsoft.FeatureManagement.Mvc; + +namespace Lab.FeatureToggle.WebAPI.Controllers; + +[ApiController] +[Route("[controller]")] +public class DemoController : ControllerBase +{ + + private readonly ILogger _logger; + private readonly IFeatureManager _featureManager; + public DemoController(ILogger logger, + IFeatureManager featureManager) + { + _logger = logger; + this._featureManager = featureManager; + } + + [HttpGet] + [FeatureGate(FeatureFlags.FeatureB)] + public async Task Get() + { + if (await _featureManager.IsEnabledAsync(FeatureFlags.FeatureA)) + { + // Run the following code + } + + return this.Ok(); + } +} + diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Demo.cs b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Demo.cs new file mode 100644 index 00000000..c4d40705 --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Demo.cs @@ -0,0 +1,35 @@ +using Microsoft.FeatureManagement; + +namespace Lab.FeatureToggle.WebAPI; + +public class Demo +{ + private readonly IFeatureManager _featureManager; + + public Demo(IFeatureManager featureManager) + { + this._featureManager = featureManager; + } + + public async Task CreateFeatureA() + { + if (await this._featureManager.IsEnabledAsync(FeatureFlags.FeatureA)) + { + //do something + return "OK"; + } + + return null; + } + public async Task CreateFeatureB() + { + if (await this._featureManager.IsEnabledAsync(FeatureFlags.FeatureB)) + { + //do something + return "OK"; + } + + return null; + } + +} \ No newline at end of file diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/DemoAsyncActionFilter.cs b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/DemoAsyncActionFilter.cs new file mode 100644 index 00000000..96825813 --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/DemoAsyncActionFilter.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Lab.FeatureToggle.WebAPI; + +public class DemoAsyncActionFilter : IAsyncActionFilter +{ + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + Console.WriteLine("on action execution"); + + // Do something before the action executes. + await next(); + + // Do something after the action executes. + } +} \ No newline at end of file diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/DemoFeatureFilter.cs b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/DemoFeatureFilter.cs new file mode 100644 index 00000000..bc7a50a3 --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/DemoFeatureFilter.cs @@ -0,0 +1,12 @@ +using Microsoft.FeatureManagement; + +namespace Lab.FeatureToggle.WebAPI; + +public class DemoFeatureFilter : IFeatureFilter +{ + public async Task EvaluateAsync(FeatureFilterEvaluationContext context) + { + // Your implementation here + return true; + } +} \ No newline at end of file diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/FeatureFlags.cs b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/FeatureFlags.cs new file mode 100644 index 00000000..d17f8be4 --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/FeatureFlags.cs @@ -0,0 +1,8 @@ +namespace Lab.FeatureToggle.WebAPI; + +public static class FeatureFlags +{ + public const string FeatureA = "FeatureA"; + public const string FeatureB = "FeatureB"; + public const string FeatureC = "FeatureC"; +} \ No newline at end of file diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Lab.FeatureToggle.WebAPI.csproj b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Lab.FeatureToggle.WebAPI.csproj new file mode 100644 index 00000000..ce5f9d3e --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Lab.FeatureToggle.WebAPI.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + enable + enable + + + + + + + + + diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Program.cs b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Program.cs new file mode 100644 index 00000000..3ed18f56 --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Program.cs @@ -0,0 +1,65 @@ +using System.Net.Http.Headers; +using System.Text; +using Lab.FeatureToggle.WebAPI; +using Microsoft.AspNetCore.Mvc; +using Microsoft.FeatureManagement; +using Microsoft.FeatureManagement.FeatureFilters; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +var environmentName = builder.Environment.EnvironmentName; +builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); +builder.Configuration.AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: true); +builder.Services.AddFeatureManagement(); +builder.Services.AddFeatureManagement() + .UseDisabledFeaturesHandler((features, context) => + { + context.Result = new ObjectResult(new + { + FailureCode = "FeatureDisabled", + FailureMessage = $"The feature {features.First()} is disabled.", + TraceId = context.HttpContext.TraceIdentifier + }) + { + StatusCode = 404 + }; + }); +builder.Services.AddFeatureManagement().AddFeatureFilter(); +builder.Services.AddFeatureManagement().AddFeatureFilter(); + +builder.Services.AddControllers(p => p.Filters.AddForFeature(FeatureFlags.FeatureB)); + +// 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.UseForFeature(FeatureFlags.FeatureA, appBuilder => +{ + appBuilder.Use(async (context, next) => + { + Console.WriteLine("on middleware execution"); + + // Do something with the request + await next.Invoke(); + + // Do something with the response + }); +}); +app.Run(); \ No newline at end of file diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Properties/launchSettings.json b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Properties/launchSettings.json new file mode 100644 index 00000000..6669fd61 --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:63863", + "sslPort": 44364 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5259", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7052;http://localhost:5259", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/WeatherForecast.cs b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/WeatherForecast.cs new file mode 100644 index 00000000..d752eeb4 --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Lab.FeatureToggle.WebAPI; + +public class WeatherForecast +{ + public DateOnly 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/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/appsettings.Development.json b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/appsettings.json b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/appsettings.json new file mode 100644 index 00000000..0b7b878a --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.WebAPI/appsettings.json @@ -0,0 +1,23 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "FeatureManagement": { + "FeatureA": true, + "FeatureB": false, + "FeatureC": { + "EnabledFor": [ + { + "Name": "Percentage", + "Parameters": { + "Value": 50 + } + } + ] + } + }, + "AllowedHosts": "*" +} diff --git a/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.sln b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.sln new file mode 100644 index 00000000..fdbcc79c --- /dev/null +++ b/Feature Toggle/Lab.FeatureToggle/Lab.FeatureToggle.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.FeatureToggle.WebAPI", "Lab.FeatureToggle.WebAPI\Lab.FeatureToggle.WebAPI.csproj", "{2CC9FEB5-2BF6-4E6A-998C-B26880EF3A72}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.FeatureToggle.TestProject", "Lab.FeatureToggle.TestProject\Lab.FeatureToggle.TestProject.csproj", "{B34F3DC3-E2D0-45A6-BE92-74DF131FE88B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2CC9FEB5-2BF6-4E6A-998C-B26880EF3A72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CC9FEB5-2BF6-4E6A-998C-B26880EF3A72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CC9FEB5-2BF6-4E6A-998C-B26880EF3A72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CC9FEB5-2BF6-4E6A-998C-B26880EF3A72}.Release|Any CPU.Build.0 = Release|Any CPU + {B34F3DC3-E2D0-45A6-BE92-74DF131FE88B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B34F3DC3-E2D0-45A6-BE92-74DF131FE88B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B34F3DC3-E2D0-45A6-BE92-74DF131FE88B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B34F3DC3-E2D0-45A6-BE92-74DF131FE88B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal 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/.dockerignore b/Graceful Shutdown/Lab.GracefulShutdown/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/.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/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Dockerfile b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Dockerfile new file mode 100644 index 00000000..2d8402c0 --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["Lab.GracefulShutdown.Net6/Lab.GracefulShutdown.Net6.csproj", "Lab.GracefulShutdown.Net6/"] +RUN dotnet restore "Lab.GracefulShutdown.Net6/Lab.GracefulShutdown.Net6.csproj" +COPY . . +WORKDIR "/src/Lab.GracefulShutdown.Net6" +RUN dotnet build "Lab.GracefulShutdown.Net6.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Lab.GracefulShutdown.Net6.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Lab.GracefulShutdown.Net6.dll"] 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..3f5ea458 --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/GracefulShutdownService.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Lab.GracefulShutdown.Net6; + +internal class GracefulShutdownService : IHostedService +{ + private readonly IHostApplicationLifetime _appLifetime; + private Task _backgroundTask; + private bool _stop; + private ILogger _logger; + + public GracefulShutdownService(IHostApplicationLifetime appLifetime, + ILogger logger) + { + this._appLifetime = appLifetime; + this._logger = logger; + } + + public Task StartAsync(CancellationToken cancel) + { + this._logger.LogInformation($"{DateTime.Now} 服務啟動中..."); + + this._backgroundTask = Task.Run(async () => { await this.ExecuteAsync(cancel); }, cancel); + + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancel) + { + this._logger.LogInformation($"{DateTime.Now} 服務停止中..."); + + this._stop = true; + await this._backgroundTask; + + this._logger.LogInformation($"{DateTime.Now} 服務已停止"); + } + + private async Task ExecuteAsync(CancellationToken cancel) + { + this._logger.LogInformation($"{DateTime.Now} 服務已啟動!"); + + while (!this._stop) + { + this._logger.LogInformation($"{DateTime.Now} 1.服務運行中..."); + this._logger.LogInformation($"1.IsCancel={cancel.IsCancellationRequested}"); + await Task.Delay(TimeSpan.FromSeconds(30), cancel); + this._logger.LogInformation($"2.IsCancel={cancel.IsCancellationRequested}"); + this._logger.LogInformation($"{DateTime.Now} 2.服務運行中..."); + } + + this._logger.LogInformation($"{DateTime.Now} 服務已完美的停止(Graceful Shutdown)"); + } +} \ No newline at end of file diff --git a/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/GracefulShutdownService1.cs b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/GracefulShutdownService1.cs new file mode 100644 index 00000000..dd59d00c --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/GracefulShutdownService1.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Lab.GracefulShutdown.Net6; + +class GracefulShutdownService1 : BackgroundService +{ + private readonly ILogger _logger; + + public GracefulShutdownService1(ILogger logger) + { + this._logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + this._logger.LogInformation($"{DateTime.Now} 服務已啟動!"); + while (!stoppingToken.IsCancellationRequested) + { + this._logger.LogInformation($"{DateTime.Now} 1.服務運行中..."); + this._logger.LogInformation($"1.IsCancel={stoppingToken.IsCancellationRequested}"); + await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); + this._logger.LogInformation($"2.IsCancel={stoppingToken.IsCancellationRequested}"); + this._logger.LogInformation($"{DateTime.Now} 2.服務運行中..."); + } + this._logger.LogInformation($"{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..01169661 --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Lab.GracefulShutdown.Net6.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + enable + enable + Linux + + + + + + + + + + + .dockerignore + + + + 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..ce32bab2 --- /dev/null +++ b/Graceful Shutdown/Lab.GracefulShutdown/Lab.GracefulShutdown.Net6/Program.cs @@ -0,0 +1,71 @@ +using System.Diagnostics; +using Lab.GracefulShutdown.Net6; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Runtime.Loader; +using Serilog; +using Serilog.Formatting.Json; + +var sigintReceived = false; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Day) + .CreateBootstrapLogger() + ; +Log.Information($"Process id: {Process.GetCurrentProcess().Id}"); +Log.Information("等待以下訊號 SIGINT/SIGTERM"); + +Console.CancelKeyPress += (sender, e) => +{ + e.Cancel = true; + Log.Information("已接收 SIGINT (Ctrl+C)"); + sigintReceived = true; +}; + +AssemblyLoadContext.Default.Unloading += ctx => +{ + if (!sigintReceived) + { + Log.Information("已接收 SIGTERM,AssemblyLoadContext.Default.Unloading"); + } + else + { + Log.Information("@AssemblyLoadContext.Default.Unloading,已處理 SIGINT,忽略 SIGTERM"); + } +}; + +AppDomain.CurrentDomain.ProcessExit += (sender, e) => +{ + if (!sigintReceived) + { + Log.Information("已接收 SIGTERM,ProcessExit"); + } + else + { + Log.Information("@AppDomain.CurrentDomain.ProcessExit,已處理 SIGINT,忽略 SIGTERM"); + } +}; + +await Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + // services.Configure(opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(15)); + // services.AddHostedService(); + services.AddHostedService(); + // services.AddHostedService(); + }) + .UseSerilog((context, services, config) => + { + var formatter = new JsonFormatter(); + config.ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console(formatter) + .WriteTo.File(formatter, "logs/app-.txt", rollingInterval: RollingInterval.Minute); + }) + .RunConsoleAsync(); + +Log.Information("下次再來唷~"); \ No newline at end of file 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/Health Check/Lab.HealthCheck/.gitignore b/Health Check/Lab.HealthCheck/.gitignore new file mode 100644 index 00000000..81c554f7 --- /dev/null +++ b/Health Check/Lab.HealthCheck/.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/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/Controllers/WeatherForecastController.cs b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..0c74e752 --- /dev/null +++ b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.HealthCheck.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/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/Lab.HealthCheck.WebApi.csproj b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/Lab.HealthCheck.WebApi.csproj new file mode 100644 index 00000000..daf2cb87 --- /dev/null +++ b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/Lab.HealthCheck.WebApi.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/Program.cs b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/Program.cs new file mode 100644 index 00000000..9d4db86c --- /dev/null +++ b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/Program.cs @@ -0,0 +1,88 @@ +using System.Net.Mime; +using System.Text.Json; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +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.AddHealthChecks(); + +builder.Services.AddHealthChecksUI(p=> + { + p.AddHealthCheckEndpoint("Readiness", "/_readiness"); + p.AddHealthCheckEndpoint("Liveness", "/_liveness"); + }) + .AddInMemoryStorage(); + +builder.Services.AddHealthChecks() + .AddUrlGroup(new Uri("https://www.google.com1"), "3rd API", tags: new[] { "3rd API", "google" }) + .AddNpgSql( + npgsqlConnectionString: "Host=localhost;Port=5432;Database=member_service;Username=postgres;Password=guest", + healthQuery: "SELECT 1;", + name: "db", + failureStatus: HealthStatus.Unhealthy, + tags: new[] { "db", "sql", "PostgreSQL" }) + ; +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); +app.MapHealthChecks("/_liveness", new HealthCheckOptions() +{ + Predicate = _ => false, //只檢查應用程式本身 + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse +}); + +app.MapHealthChecks("/_readiness", + new HealthCheckOptions() + { + //檢查應用程式所依賴的服務 + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + + // ResponseWriter = (context, report) => + // { + // context.Response.ContentType = MediaTypeNames.Text.Plain; + // return context.Response.WriteAsync("OK"); + // } + + // ResponseWriter = async (context, report) => + // { + // var result = JsonSerializer.Serialize( + // new + // { + // status = report.Status.ToString(), + // errors = report.Entries + // .Select(e => + // new + // { + // key = e.Key, + // value = Enum.GetName(typeof(HealthStatus), e.Value.Status) + // }) + // }); + // context.Response.ContentType = MediaTypeNames.Application.Json; + // await context.Response.WriteAsync(result); + // } + }); + +app.UseHealthChecksUI(options => { options.UIPath = "/_hc"; }); + +app.Run(); \ No newline at end of file diff --git a/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/WeatherForecast.cs b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/WeatherForecast.cs new file mode 100644 index 00000000..a58e9e2b --- /dev/null +++ b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Lab.HealthCheck.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/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/appsettings.Development.json b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/appsettings.json b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/appsettings.json new file mode 100644 index 00000000..4af1a8c6 --- /dev/null +++ b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.WebApi/appsettings.json @@ -0,0 +1,23 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "//HealthChecks-UI": { + "HealthChecks": [ + { + "Name": "Readiness1", + "Uri": "_readiness" + }, + { + "Name": "Liveness", + "Uri": "_liveness" + } + ], + "EvaluationTimeOnSeconds": 10, + "MinimumSecondsBetweenFailureNotifications": 60 + } +} diff --git a/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.sln b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.sln new file mode 100644 index 00000000..23b90e0f --- /dev/null +++ b/Health Check/Lab.HealthCheck/src/Lab.HealthCheck.sln @@ -0,0 +1,21 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.HealthCheck.WebApi", "Lab.HealthCheck.WebApi\Lab.HealthCheck.WebApi.csproj", "{2DF0910D-04FC-4EF7-995C-3708D2BB5F4C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{2695B84D-A033-4A82-853A-3A28997D52B9}" + ProjectSection(SolutionItems) = preProject + ..\.gitignore = ..\.gitignore + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2DF0910D-04FC-4EF7-995C-3708D2BB5F4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DF0910D-04FC-4EF7-995C-3708D2BB5F4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DF0910D-04FC-4EF7-995C-3708D2BB5F4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DF0910D-04FC-4EF7-995C-3708D2BB5F4C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal 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/Http Client/Lab.HttpClientWithCookie/.gitignore b/Http Client/Lab.HttpClientWithCookie/.gitignore new file mode 100644 index 00000000..333fe87e --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/.gitignore @@ -0,0 +1,671 @@ +### ASPNETCore template +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# 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 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# 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 + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.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 + +# 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 + +# 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 +# TODO: 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 +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable 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 + +# 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 +node_modules/ +orleans.codegen.cs + +# 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 + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# 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/ + +### Csharp 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/main/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 +*.tlog +*.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 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# 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/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# 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 + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +./logs/ \ No newline at end of file diff --git a/Http Client/Lab.HttpClientWithCookie/Taskfile.yml b/Http Client/Lab.HttpClientWithCookie/Taskfile.yml new file mode 100644 index 00000000..37d900a6 --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/Taskfile.yml @@ -0,0 +1,27 @@ +# Taskfile.yml + +version: "3" + +tasks: + demo: + desc: WebApi Development + cmds: + - task: demo1 + - task: demo2 + + WebAppA: + desc: WebApi Development + dir: "src/WebAppA" + cmds: + - dotnet run --environment Staging + + WebAppB: + desc: WebApi Development + dir: "src/WebAppB" + cmds: + - dotnet run --environment Staging + + 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/Http Client/Lab.HttpClientWithCookie/src/Lab.HttpClientWithCookie.sln b/Http Client/Lab.HttpClientWithCookie/src/Lab.HttpClientWithCookie.sln new file mode 100644 index 00000000..4662c59b --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/src/Lab.HttpClientWithCookie.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAppA", "WebAppA\WebAppA.csproj", "{CF4A808A-1217-4CD6-9B14-483E218C7497}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAppB", "WebAppB\WebAppB.csproj", "{C54987B2-7F8B-400E-B210-C4EE5460F1B7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{2076DDF6-5000-4E44-8CE1-4320085633D4}" + ProjectSection(SolutionItems) = preProject + ..\Taskfile.yml = ..\Taskfile.yml + ..\.gitignore = ..\.gitignore + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CF4A808A-1217-4CD6-9B14-483E218C7497}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF4A808A-1217-4CD6-9B14-483E218C7497}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF4A808A-1217-4CD6-9B14-483E218C7497}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF4A808A-1217-4CD6-9B14-483E218C7497}.Release|Any CPU.Build.0 = Release|Any CPU + {C54987B2-7F8B-400E-B210-C4EE5460F1B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C54987B2-7F8B-400E-B210-C4EE5460F1B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C54987B2-7F8B-400E-B210-C4EE5460F1B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C54987B2-7F8B-400E-B210-C4EE5460F1B7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Http Client/Lab.HttpClientWithCookie/src/WebAppA/Controllers/Demo1Controller.cs b/Http Client/Lab.HttpClientWithCookie/src/WebAppA/Controllers/Demo1Controller.cs new file mode 100644 index 00000000..9996e2bc --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/src/WebAppA/Controllers/Demo1Controller.cs @@ -0,0 +1,179 @@ +using System.Diagnostics; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; + +namespace WebAppA.Controllers; + +public class LabService : ILabService +{ + private readonly HttpClient _client; + + public LabService(HttpClient client) + { + this._client = client; + } + + public IEnumerable Get() + { + var url = "api/default"; + var response = this._client.GetAsync(url).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + + var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + var result = JsonSerializer.Deserialize(content); + + return result; + } +} + +public interface ILabService +{ + IEnumerable Get(); +} + +[ApiController] + +// [Route("[controller]")] +public class Demo1Controller : ControllerBase +{ + private readonly ILogger _logger; + private readonly HttpClient _client; + + private readonly HttpClient s_client = new HttpClient + { + BaseAddress = new Uri(serverUrl), + }; + + private readonly IHttpClientFactory _httpClientFactory; + static string serverUrl = "https://localhost:7004/"; + private ILabService _service; + + // public Demo1Controller(ILogger logger, ILabService service) + // { + // this._logger = logger; + // this._service = service; + // } + + public Demo1Controller(ILogger logger, IHttpClientFactory httpClientFactory) + { + this._logger = logger; + this._httpClientFactory = httpClientFactory; + } + + [HttpGet] + [Route("/api/demo1")] + public async Task Get() + { + var requestId = this.HttpContext.TraceIdentifier; + + //header + if (this.Request.Headers.Any()) + { + this._logger.LogInformation("1.Id={@RequestId}, At 'Demo1', Receive request headers ={@Headers}", requestId, + this.Request.Headers); + } + + //cookie + if (this.Request.Cookies.Any()) + { + this._logger.LogInformation("2.Id={@RequestId}, At 'Demo1', Receive request cookie ={@Cookies}", requestId, + this.Request.Cookies); + } + + var url = "api/Demo2"; + + // var client = this._client; + + // var client = this.s_client; + + var client = this._httpClientFactory.CreateClient("lab"); + + // var client = new HttpClient() + // { + // BaseAddress = new Uri(serverUrl) + // }; + + //送出並行請求 + // await SendMultiRequest(client, url); + + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Headers = + { + { "A", "123" }, + } + }; + var response = await client.SendAsync(request); + + if (response.Headers.Any()) + { + this._logger.LogInformation("3.Id={@RequestId}, At 'Demo1', Receive response headers ={@Headers}", + requestId, + response.Headers); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + if (string.IsNullOrWhiteSpace(responseContent) == false) + { + this._logger.LogInformation("4.Id={@RequestId}, At 'Demo1', Receive response body ={@Body}", + requestId, + responseContent); + } + + // if (response.Headers.TryGetValues("A", out var a2)) + // { + // this._logger.LogInformation("Id={RequestId},Response Header 'A'={Data}", requestId, a2); + // } + // + // if (response.Headers.TryGetValues("B", out var b2)) + // { + // this._logger.LogInformation("Id={RequestId},Response Header 'B'={Data}", requestId, b2); + // } + // + // if (response.Headers.TryGetValues("C", out var c2)) + // { + // this._logger.LogInformation("Id={RequestId},Response Header 'C'={Data}", requestId, c2); + // } + // + // if (response.Headers.TryGetValues("D", out var d2)) + // { + // this._logger.LogInformation("Id={RequestId},Response Header 'D'={Data}", requestId, d2); + // } + // + // //取得 Response Cookies + // if (response.Headers.TryGetValues("Set-Cookie", out var cookies)) + // { + // this._logger.LogInformation("Id={RequestId},Response Cookie 'All Cookie'={Data}", requestId, cookies); + // } + return this.Ok(); + } + + private static async Task SendParallelRequest(HttpClient client, string url) + { + var processTime = new Stopwatch(); + processTime.Start(); + var tasks = new List(); + while (true) + { + //送出並行請求 + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Headers = + { + { "A", "123" }, + } + }; + + var task = client.SendAsync(request); + tasks.Add(task); + if (processTime.Elapsed.TotalSeconds > 5) + { + //五秒後就停止 + processTime.Stop(); + break; + } + } + + await Task.WhenAll(tasks); + } +} \ No newline at end of file diff --git a/Http Client/Lab.HttpClientWithCookie/src/WebAppA/Program.cs b/Http Client/Lab.HttpClientWithCookie/src/WebAppA/Program.cs new file mode 100644 index 00000000..9aa06241 --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/src/WebAppA/Program.cs @@ -0,0 +1,113 @@ +using Serilog; +using Serilog.Events; +using Serilog.Formatting.Json; +using WebAppA.Controllers; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Day) + .CreateBootstrapLogger() + ; +try +{ + Log.Information("Starting web host"); + + var builder = WebApplication.CreateBuilder(args); + + // builder.Host.UseSerilog(); //<=== 讓 Host 使用 Serilog + var formatter = new JsonFormatter(); + + // var formatter = new MessageTemplateTextFormatter(); + // var formatter = new RawFormatter(); + // var formatter = new RenderedCompactJsonFormatter(); + // var formatter = new CompactJsonFormatter(); + // var formatter = new ExpressionTemplate( + // "{ {_t: @t, _msg: @m, _props: @p} }\n"); + builder.Host.UseSerilog((context, services, config) => + { + config.ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console(formatter) + .WriteTo.Seq("http://localhost:5341") + .WriteTo.File(formatter, "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 serverUrl = "https://localhost:7004/"; + + // // ---會快取 cookie--- + // 建構子依賴 IHttpClientFactory + builder.Services.AddHttpClient("lab", + p => { p.BaseAddress = new Uri(serverUrl); }); + // // ---會快取 cookie--- + + // // ---不會快取 cookie--- + // // 建構子依賴 HttpClient + // builder.Services.AddSingleton(p => + // { + // var socketsHandler = new SocketsHttpHandler(); + // return new HttpClient(socketsHandler) + // { + // BaseAddress = new Uri(serverUrl) + // }; + // }); + // // ---不會快取 cookie--- + + // // ---不會快取 cookie--- + // builder.Services.AddHttpClient("lab", + // p => { p.BaseAddress = new Uri(serverUrl); }) + // .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler() + // { + // // 改成 true,會快取 Cookie + // UseCookies = false, + // }); + // // ---不會快取 cookie--- + + // // ---不會快取 cookie--- + // builder.Services.AddSingleton(p => new HttpClient + // { + // BaseAddress = new Uri(serverUrl) + // }); + // // ---不會快取 cookie--- + + builder.Services.AddScoped(); + 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/Http Client/Lab.HttpClientWithCookie/src/WebAppA/WebAppA.csproj b/Http Client/Lab.HttpClientWithCookie/src/WebAppA/WebAppA.csproj new file mode 100644 index 00000000..901aaf53 --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/src/WebAppA/WebAppA.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + enable + enable + + + + + + + + + + diff --git a/Http Client/Lab.HttpClientWithCookie/src/WebAppA/appsettings.Development.json b/Http Client/Lab.HttpClientWithCookie/src/WebAppA/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/src/WebAppA/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Http Client/Lab.HttpClientWithCookie/src/WebAppA/appsettings.json b/Http Client/Lab.HttpClientWithCookie/src/WebAppA/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/src/WebAppA/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Http Client/Lab.HttpClientWithCookie/src/WebAppB/Controllers/Demo2Controller.cs b/Http Client/Lab.HttpClientWithCookie/src/WebAppB/Controllers/Demo2Controller.cs new file mode 100644 index 00000000..40d4f37e --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/src/WebAppB/Controllers/Demo2Controller.cs @@ -0,0 +1,142 @@ +using Microsoft.AspNetCore.Mvc; + +namespace WebAppB.Controllers; + +[ApiController] +public class Demo2Controller : ControllerBase +{ + private readonly ILogger _logger; + + public Demo2Controller(ILogger logger) + { + this._logger = logger; + } + + static long s_counter = 1; + static object s_lock = new(); + static Random s_random = new(); + + [HttpGet] + [Route("/api/demo2")] + public async Task Get() + { + var identifier = this.HttpContext.TraceIdentifier; + + var cookies = this.ShouldWithoutCookies(); + var headers = this.ShouldWithoutHeaders(); + if (cookies.Any() + || headers.Any()) + { + // 找到非預期的 header/cookie 則回傳 + return this.Ok(new + { + Headers = headers.Any() ? headers : null, + Cookies = cookies.Any() ? cookies : null, + }); + } + + if (this.Request.Headers.TryGetValue("A", out var context)) + { + var data = context.FirstOrDefault(); + + //取得 Request Id + this._logger.LogInformation("1.Id={@RequestId}, At 'Demo2', Receive request headers[A] ={@Data}", + identifier, + data); + + this.Response.Headers.Add("B", data); + this.Response.Cookies.Append("B", data); + + //每一個請求都會有一個獨立的自動增量數字 + lock (s_lock) + { + s_counter++; + this.Response.Headers.Add("C", s_counter.ToString()); + this.Response.Cookies.Append("C", s_counter.ToString()); + } + + this.Response.Headers.Add("D", s_random.NextInt64(1000).ToString()); + this.Response.Cookies.Append("D", s_random.NextInt64(1000).ToString()); + } + + return this.NoContent(); + } + + private Dictionary ShouldWithoutHeaders() + { + var identifier = this.HttpContext.TraceIdentifier; + var result = new Dictionary(); + if (this.Request.Headers.TryGetValue("B", out var b)) + { + this._logger.LogError("1.Id={RequestId}, At 'Demo2', Receive request headers[B] ={Data}", + identifier, + b); + result.Add("B", b); + } + + if (this.Request.Headers.TryGetValue("C", out var c)) + { + this._logger.LogError("2.Id={RequestId}, At 'Demo2', Receive request headers[C] ={Data}", + identifier, + c); + result.Add("C", c); + } + + if (this.Request.Headers.TryGetValue("D", out var d)) + { + this._logger.LogError("3.Id={RequestId}, At 'Demo2', Receive request headers[D] ={Data}", + identifier, + d); + result.Add("D", d); + } + + return result; + } + + private Dictionary ShouldWithoutCookies() + { + var result = new Dictionary(); + var identifier = this.HttpContext.TraceIdentifier; + if (this.Request.Cookies.TryGetValue("B", out var b)) + { + this._logger.LogError("1.Id={RequestId}, At 'Demo2', Receive request cookies[B] ={Data}", + identifier, + b); + result.Add("B", b); + } + + if (this.Request.Cookies.TryGetValue("C", out var c)) + { + this._logger.LogError("2.Id={RequestId}, At 'Demo2', Receive request cookies[C] ={Data}", + identifier, + c); + result.Add("C", c); + } + + if (this.Request.Cookies.TryGetValue("D", out var d)) + { + this._logger.LogError("3.Id={RequestId}, At 'Demo2', Receive request cookies[D] ={Data}", + identifier, + d); + result.Add("D", d); + } + + if (this.Request.Cookies.TryGetValue("E", out var e)) + { + this._logger.LogError("4.Id={RequestId}, At 'Demo2', Receive request cookies[E] ={Data}", + identifier, + e); + result.Add("E", e); + } + + if (this.Request.Cookies.TryGetValue("5.F", out var f)) + { + this._logger.LogError("Id={RequestId}, At 'Demo2', Receive request cookies[F] ={Data}", + identifier, + f); + result.Add("F", f); + } + + return result; + } +} \ No newline at end of file diff --git a/Http Client/Lab.HttpClientWithCookie/src/WebAppB/Program.cs b/Http Client/Lab.HttpClientWithCookie/src/WebAppB/Program.cs new file mode 100644 index 00000000..712fb31f --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/src/WebAppB/Program.cs @@ -0,0 +1,72 @@ +using Serilog; +using Serilog.Events; +using Serilog.Formatting.Json; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Day) + .CreateBootstrapLogger() + ; +try +{ + Log.Information("Starting web host"); + + var builder = WebApplication.CreateBuilder(args); + + // builder.Host.UseSerilog(); //<=== 讓 Host 使用 Serilog + var formatter = new JsonFormatter(); + // var formatter = new MessageTemplateTextFormatter(); + // var formatter = new RawFormatter(); + // var formatter = new RenderedCompactJsonFormatter(); + // var formatter = new CompactJsonFormatter(); + // var formatter = new ExpressionTemplate( + // "{ {_t: @t, _msg: @m, _props: @p} }\n"); + builder.Host.UseSerilog((context, services, config) => + { + config.ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console(formatter) + .WriteTo.Seq("http://localhost:5341") + .WriteTo.File(formatter, "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/Http Client/Lab.HttpClientWithCookie/src/WebAppB/WebAppB.csproj b/Http Client/Lab.HttpClientWithCookie/src/WebAppB/WebAppB.csproj new file mode 100644 index 00000000..901aaf53 --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/src/WebAppB/WebAppB.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + enable + enable + + + + + + + + + + diff --git a/Http Client/Lab.HttpClientWithCookie/src/WebAppB/appsettings.Development.json b/Http Client/Lab.HttpClientWithCookie/src/WebAppB/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/src/WebAppB/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Http Client/Lab.HttpClientWithCookie/src/WebAppB/appsettings.json b/Http Client/Lab.HttpClientWithCookie/src/WebAppB/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Http Client/Lab.HttpClientWithCookie/src/WebAppB/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Jetbrains HttpClient/Jetbrains HttpClient/Jetbrains HttpClient.sln b/Jetbrains HttpClient/Jetbrains HttpClient/Jetbrains HttpClient.sln new file mode 100644 index 00000000..5402b47d --- /dev/null +++ b/Jetbrains HttpClient/Jetbrains HttpClient/Jetbrains HttpClient.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication1", "WebApplication1\WebApplication1.csproj", "{EC875AA1-C496-41D9-A613-BBDF2D0CE3CF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EC875AA1-C496-41D9-A613-BBDF2D0CE3CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC875AA1-C496-41D9-A613-BBDF2D0CE3CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC875AA1-C496-41D9-A613-BBDF2D0CE3CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC875AA1-C496-41D9-A613-BBDF2D0CE3CF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/Controllers/WeatherForecastController.cs b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..3237925c --- /dev/null +++ b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/Controllers/WeatherForecastController.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication1.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() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + + [HttpGet] + [Route("get:id")] + public ActionResult GetId() + { + return this.Ok(new + { + Id = new Random().Next(1, 5) + }); + } + + [HttpGet] + [Route("{id:int}")] + public ActionResult Get(int id) + { + var data = Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Id = index, + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + var result = data.FirstOrDefault(p => p.Id == id); + return this.Ok(result); + } +} \ No newline at end of file diff --git a/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/Program.cs b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/Program.cs new file mode 100644 index 00000000..329fe361 --- /dev/null +++ b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/Program.cs @@ -0,0 +1,26 @@ +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 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/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/Properties/launchSettings.json b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/Properties/launchSettings.json new file mode 100644 index 00000000..df2005f4 --- /dev/null +++ b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14966", + "sslPort": 44347 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5109", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7072;http://localhost:5109", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/WeatherForecast.cs b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/WeatherForecast.cs new file mode 100644 index 00000000..f9419da2 --- /dev/null +++ b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/WeatherForecast.cs @@ -0,0 +1,14 @@ +namespace WebApplication1; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + + public int Id { get; set; } +} \ No newline at end of file diff --git a/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/WebApplication1.csproj b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/WebApplication1.csproj new file mode 100644 index 00000000..fe2231a3 --- /dev/null +++ b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/WebApplication1.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + + + + + + + + diff --git a/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/appsettings.Development.json b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/appsettings.json b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/http-client.env.json b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/http-client.env.json new file mode 100644 index 00000000..7b87960a --- /dev/null +++ b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/http-client.env.json @@ -0,0 +1,8 @@ +{ + "Dev": { + "BaseUrl": "https://localhost:44347/" + }, + "Staging": { + "BaseUrl": "https://staging.example.com/" + } +} \ No newline at end of file diff --git a/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/test.http b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/test.http new file mode 100644 index 00000000..16793797 --- /dev/null +++ b/Jetbrains HttpClient/Jetbrains HttpClient/WebApplication1/test.http @@ -0,0 +1,25 @@ + +### 宣告變數 +< {% + request.variables.set("BaseUrl", "https://localhost:44347/") +%} +GET {{BaseUrl}}/WeatherForecast + +ㄕ +### 取得所有 +GET {{BaseUrl}}/WeatherForecast + +### 取得id +GET {{BaseUrl}}/WeatherForecast/get:id + +> {% + client.global.set("id", response.body.id); + client.log("result: " + response.body.id); + client.test("Request executed successfully", function () { + client.log(response.body.id); + client.assert(response.status === 201, "Response status is not 200"); + }); +%} + +### 依照上一動作取得的 id 取得資料 +GET {{BaseUrl}}/WeatherForecast/{{id}} \ No newline at end of file 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/Json/Lab.JsonMergePatch/.gitignore b/Json/Lab.JsonMergePatch/.gitignore new file mode 100644 index 00000000..031950aa --- /dev/null +++ b/Json/Lab.JsonMergePatch/.gitignore @@ -0,0 +1 @@ +launchSettings.json diff --git a/Json/Lab.JsonMergePatch/Lab.JsonMergePatch.sln b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch.sln new file mode 100644 index 00000000..8ade454a --- /dev/null +++ b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.JsonMergePatch", "Lab.JsonMergePatch\Lab.JsonMergePatch.csproj", "{ED23C1AA-6707-4019-B427-EEEEB754B140}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {ED23C1AA-6707-4019-B427-EEEEB754B140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED23C1AA-6707-4019-B427-EEEEB754B140}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED23C1AA-6707-4019-B427-EEEEB754B140}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED23C1AA-6707-4019-B427-EEEEB754B140}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Controllers/EmployeeController.cs b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Controllers/EmployeeController.cs new file mode 100644 index 00000000..ba488b42 --- /dev/null +++ b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Controllers/EmployeeController.cs @@ -0,0 +1,59 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Lab.JsonMergePatch.Models; +using Microsoft.AspNetCore.Mvc; +using Morcatko.AspNetCore.JsonMergePatch; +using Swashbuckle.AspNetCore.Filters; + +namespace Lab.JsonMergePatch.Controllers; + +[ApiController] +[Route("[controller]")] +public class EmployeeController : ControllerBase +{ + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonSerializerOptions; + + public EmployeeController(ILogger logger, + JsonSerializerOptions jsonSerializerOptions) + { + this._logger = logger; + this._jsonSerializerOptions = jsonSerializerOptions; + } + + [HttpGet] + public async Task Get() + { + return this.Ok(this.GetEmployee()); + } + + [HttpPatch] + [SwaggerRequestExample(typeof(PatchEmployeeRequest), typeof(PatchEmployeeRequest.PatchEmployeeRequestExample))] + public async Task Patch(JsonMergePatchDocument request) + { + var original = this.GetEmployee(); + var patchResult = request.ApplyToT(original); + return this.Ok(patchResult); + } + + Employee GetEmployee() + { + var now = DateTimeOffset.Now; + var userId = "Sys"; + return new Employee + { + Id = Guid.NewGuid(), + Address = new Address + { + Address1 = "台北市", + Address2 = "大安區", + Street = "忠孝東路" + }, + Birthday = new DateTime(2009, 12, 25), + CreatedAt = now, + CreatedBy = userId, + ModifiedAt = now, + ModifiedBy = userId, + }; + } +} \ No newline at end of file diff --git a/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/JsonSerializeFactory.cs b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/JsonSerializeFactory.cs new file mode 100644 index 00000000..a62de48c --- /dev/null +++ b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/JsonSerializeFactory.cs @@ -0,0 +1,25 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lab.JsonMergePatch; + +public class JsonSerializeFactory +{ + public static JsonSerializerOptions CreateDefaultOptions() + { + var options = new JsonSerializerOptions(); + Apply(options); + return options; + } + + public static JsonSerializerOptions Apply(JsonSerializerOptions options) + { + options.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + options.PropertyNameCaseInsensitive = true; + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + return options; + } +} \ No newline at end of file diff --git a/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Lab.JsonMergePatch.csproj b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Lab.JsonMergePatch.csproj new file mode 100644 index 00000000..62fc19d6 --- /dev/null +++ b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Lab.JsonMergePatch.csproj @@ -0,0 +1,16 @@ + + + + net7.0 + enable + enable + + + + + + + + + + diff --git a/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/Address.cs b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/Address.cs new file mode 100644 index 00000000..d79b75ed --- /dev/null +++ b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/Address.cs @@ -0,0 +1,10 @@ +namespace Lab.JsonMergePatch.Models; + +public class Address +{ + public string? Address1 { get; set; } + + public string? Address2 { get; set; } + + public string? Street { get; set; } +} \ No newline at end of file diff --git a/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/Employee.cs b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/Employee.cs new file mode 100644 index 00000000..f479776b --- /dev/null +++ b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/Employee.cs @@ -0,0 +1,22 @@ +namespace Lab.JsonMergePatch.Models; + +public class Employee +{ + public Guid Id { get; set; } + + public string? Name { get; set; } + + public Address? Address { get; set; } + + public DateTime? Birthday { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset? ModifiedAt { get; set; } + + public string? ModifiedBy { get; set; } + + public List Extensions { get; set; } = new(); +} \ No newline at end of file diff --git a/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/Name.cs b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/Name.cs new file mode 100644 index 00000000..5c42fd23 --- /dev/null +++ b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/Name.cs @@ -0,0 +1,10 @@ +namespace Lab.JsonMergePatch.Models; + +public class Name +{ + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string NickName { get; set; } +} \ No newline at end of file diff --git a/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/PatchEmployeeRequest.cs b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/PatchEmployeeRequest.cs new file mode 100644 index 00000000..917bfdf9 --- /dev/null +++ b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Models/PatchEmployeeRequest.cs @@ -0,0 +1,33 @@ +using Swashbuckle.AspNetCore.Filters; + +namespace Lab.JsonMergePatch.Models; + +public class PatchEmployeeRequest +{ + public string? Name { get; set; } + + public Address? Address { get; set; } + + public DateTime? Birthday { get; set; } + + public List Extensions { get; set; } = new(); + + public class PatchEmployeeRequestExample : IExamplesProvider + { + public PatchEmployeeRequest GetExamples() + { + return new PatchEmployeeRequest + { + Name = "小章", + Address = new Address + { + Address1 = "台北市", + Address2 = "大安區", + Street = "忠孝東路" + }, + Birthday = DateTime.Today, + Extensions = new List() { "ex1", "ex2" } + }; + } + } +} \ No newline at end of file diff --git a/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Program.cs b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Program.cs new file mode 100644 index 00000000..4acf21f3 --- /dev/null +++ b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/Program.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using Lab.JsonMergePatch; +using Morcatko.AspNetCore.JsonMergePatch; +using Swashbuckle.AspNetCore.Filters; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers() + .AddJsonOptions(opts => JsonSerializeFactory.Apply(opts.JsonSerializerOptions)) + .AddSystemTextJsonMergePatch(p => p.EnableDelete = true); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(p => p.ExampleFilters()); +builder.Services.AddSwaggerExamplesFromAssemblies(Assembly.GetEntryAssembly()); +builder.Services.AddSingleton(p => JsonSerializeFactory.CreateDefaultOptions()); +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/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/appsettings.Development.json b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/appsettings.json b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Json/Lab.JsonMergePatch/Lab.JsonMergePatch/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Json/Lab.JsonPartialCompare/Lab.JsonPartialCompare.Test/Lab.JsonPartialCompare.Test.csproj b/Json/Lab.JsonPartialCompare/Lab.JsonPartialCompare.Test/Lab.JsonPartialCompare.Test.csproj new file mode 100644 index 00000000..56c36b5a --- /dev/null +++ b/Json/Lab.JsonPartialCompare/Lab.JsonPartialCompare.Test/Lab.JsonPartialCompare.Test.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + + false + + default + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Json/Lab.JsonPartialCompare/Lab.JsonPartialCompare.Test/UnitTest1.cs b/Json/Lab.JsonPartialCompare/Lab.JsonPartialCompare.Test/UnitTest1.cs new file mode 100644 index 00000000..00d27e81 --- /dev/null +++ b/Json/Lab.JsonPartialCompare/Lab.JsonPartialCompare.Test/UnitTest1.cs @@ -0,0 +1,165 @@ +using System.Text.Json.Nodes; +using Xunit; + +namespace Lab.JsonPartialCompare.Test; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + var leftJson = + """ + { + "Id": 1, + "Birthday": "2000-01-01T00:00:00+00:00", + "FullName": { + "LastName": "Doe" + } + } + """; + + var rightJson = + """ + { + "Id": 1, + "Age": 18, + "Birthday": "2000-01-01T00:00:00+00:00", + "FullName": { + "FirstName": "John", + "LastName": "Doe" + }, + "Addresses": [ + { + "Address1": "Address1", + "Address2": "Address2" + } + ] + } + """; + var leftNode = JsonNode.Parse(leftJson); + var rightNode = JsonNode.Parse(rightJson); + + var result = JsonCompare.Diff(leftNode, rightNode); + Assert.True(result.IsValidated, result.ErrorReason); + } +} + +class JsonCompare +{ + public static JsonDiffResult Diff(JsonNode left, JsonNode right) + { + if (left is JsonObject leftObject + && right is JsonObject rightObject) + { + foreach (var property in leftObject) + { + if (!rightObject.TryGetPropertyValue(property.Key, out JsonNode rightProperty)) + { + return new JsonDiffResult(false, $"Property '{property.Key}' is missing in the right JSON."); + } + + var result = Diff(property.Value, rightProperty); + if (!result.IsValidated) + { + return new JsonDiffResult(false, $"Property '{property.Key}' mismatch: {result.ErrorReason}"); + } + } + + return new JsonDiffResult(true, null); + } + + if (left is JsonArray leftArray + && right is JsonArray rightArray) + { + if (leftArray.Count != rightArray.Count) + { + return new JsonDiffResult(false, "Array length mismatch."); + } + + for (var i = 0; i < leftArray.Count; i++) + { + var result = Diff(leftArray[i], rightArray[i]); + if (!result.IsValidated) + { + return new JsonDiffResult(false, + $"Array element at index {i} mismatch: {result.ErrorReason}"); + } + } + + return new JsonDiffResult(true, null); + } + + if (left.ToJsonString() != right.ToJsonString()) + { + return new JsonDiffResult(false, + $"Value mismatch: '{left.ToJsonString()}' vs '{right.ToJsonString()}'"); + } + + return new JsonDiffResult(true, null); + } +} + +class JsonDiffResult +{ + public bool IsValidated { get; } + + public string ErrorReason { get; } + + public JsonDiffResult(bool isValidated, string errorReason) + { + this.IsValidated = isValidated; + this.ErrorReason = errorReason; + } + + static JsonDiffResult JsonCompare(JsonNode left, JsonNode right) + { + if (left is JsonObject leftObject + && right is JsonObject rightObject) + { + foreach (var property in leftObject) + { + if (!rightObject.TryGetPropertyValue(property.Key, out JsonNode rightProperty)) + { + return new JsonDiffResult(false, $"Property '{property.Key}' is missing in the right JSON."); + } + + var result = JsonCompare(property.Value, rightProperty); + if (!result.IsValidated) + { + return new JsonDiffResult(false, $"Property '{property.Key}' mismatch: {result.ErrorReason}"); + } + } + + return new JsonDiffResult(true, null); + } + + if (left is JsonArray leftArray && right is JsonArray rightArray) + { + if (leftArray.Count != rightArray.Count) + { + return new JsonDiffResult(false, "Array length mismatch."); + } + + for (var i = 0; i < leftArray.Count; i++) + { + var result = JsonCompare(leftArray[i], rightArray[i]); + if (!result.IsValidated) + { + return new JsonDiffResult(false, + $"Array element at index {i} mismatch: {result.ErrorReason}"); + } + } + + return new JsonDiffResult(true, null); + } + + if (left.ToJsonString() != right.ToJsonString()) + { + return new JsonDiffResult(false, + $"Value mismatch: '{left.ToJsonString()}' vs '{right.ToJsonString()}'"); + } + + return new JsonDiffResult(true, null); + } +} \ No newline at end of file diff --git a/Json/Lab.JsonPartialCompare/Lab.JsonPartialCompare.sln b/Json/Lab.JsonPartialCompare/Lab.JsonPartialCompare.sln new file mode 100644 index 00000000..c26ba0f3 --- /dev/null +++ b/Json/Lab.JsonPartialCompare/Lab.JsonPartialCompare.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.JsonPartialCompare.Test", "Lab.JsonPartialCompare.Test\Lab.JsonPartialCompare.Test.csproj", "{FB77556E-4D63-4166-B97E-39BE135EAA88}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FB77556E-4D63-4166-B97E-39BE135EAA88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB77556E-4D63-4166-B97E-39BE135EAA88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB77556E-4D63-4166-B97E-39BE135EAA88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB77556E-4D63-4166-B97E-39BE135EAA88}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/BaseStep.cs b/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/BaseStep.cs new file mode 100644 index 00000000..e5fd5685 --- /dev/null +++ b/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/BaseStep.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.JsonDiffPatch; +using System.Text.Json.JsonDiffPatch.Xunit; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using FluentAssertions; +using Gherkin.Ast; +using Json.Path; +using TechTalk.SpecFlow; +using TechTalk.SpecFlow.Assist; +using Xunit; + +namespace Lab.JsonPathForTestCase.Test; + +[Binding] +public class BaseStep : Steps +{ + class Member + { + public int Id { get; set; } + + public int Age { get; set; } + + public DateTimeOffset Birthday { get; set; } + + public FullName FullName { get; set; } + } + + class FullName + { + public string FirstName { get; set; } + + public string LastName { get; set; } + } + + private const string StringEquals = "字串等於"; + private const string NumberEquals = "數值等於"; + private const string BoolEquals = "布林值等於"; + private const string JsonEquals = "Json等於"; + private const string DateTimeEquals = "時間等於"; + + private const string OperationTypes = StringEquals + + "|" + NumberEquals + + "|" + BoolEquals + + "|" + JsonEquals + + "|" + DateTimeEquals; + + [Given(@"資料庫的 Member 資料表已經存在")] + public void Given資料庫的Member資料表已經存在(Table table) + { + var members = table.CreateSet(); + this.ScenarioContext.Set(members); + } + + [Then(@"預期資料庫的 Member 資料表應該有")] + public void Then預期資料庫的Member資料表應該有(Table table) + { + var actual = this.ScenarioContext.Get>(); + table.CompareToSet(actual); + } + + [Then(@"預期回傳內容中路徑 ""(.*)"" 的(" + OperationTypes + @") ""(.*)""")] + public void Then預期回傳內容中路徑的字串等於(string selector, string operationType, string expected) + { + var data = this.ScenarioContext.Get(); + var json = JsonSerializer.Serialize(data); + ContentShouldBe(json, selector, operationType, expected); + } + + [When(@"調用端發送 ""(.*)"" 請求至 ""(.*)""")] + public void When調用端發送請求至(string httpMethod, string url) + { + var data = new Member + { + Id = 1, + Age = 18, + Birthday = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), + FullName = new FullName + { + FirstName = "John", + LastName = "Doe" + } + }; + this.ScenarioContext.Set(data); + } + + private static void ContentShouldBe(string content, string selectPath, string operationType, string expected) + { + var srcInstance = JsonNode.Parse(content); + var jsonPath = JsonPath.Parse(selectPath); + switch (operationType) + { + case StringEquals: + { + var actual = jsonPath.Evaluate(srcInstance).Matches.FirstOrDefault()?.Value?.GetValue(); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]"; + (actual ?? string.Empty).Should().Be(expected, errorReason); + break; + } + case NumberEquals: + { + var actual = jsonPath.Evaluate(srcInstance).Matches.FirstOrDefault()?.Value?.GetValue(); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]"; + actual.Should().Be(int.Parse(expected), errorReason); + break; + } + case BoolEquals: + { + var actual = jsonPath.Evaluate(srcInstance).Matches.FirstOrDefault()?.Value?.GetValue(); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]"; + actual.Should().Be(bool.Parse(expected), errorReason); + break; + } + case DateTimeEquals: + { + var expect = DateTimeOffset.Parse(expected); + var actual = jsonPath.Evaluate(srcInstance).Matches.FirstOrDefault() + ?.Value + ?.GetValue() + ; + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expect}], {nameof(actual)}: [{actual}]"; + actual.Should().Be(expect, errorReason); + break; + } + case JsonEquals: + { + var actual = jsonPath.Evaluate(srcInstance).Matches.FirstOrDefault()?.Value; + var expect = string.IsNullOrWhiteSpace(expected) ? null : JsonNode.Parse(expected); + var diff = actual.Diff(expect); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual?.ToJsonString()}], diff: [{diff?.ToJsonString()}]"; + actual.DeepEquals(expect).Should().BeTrue(errorReason); + break; + } + } + } + + [Then(@"預期得到回傳 Member 結果為")] + public void Then預期得到回傳Member結果為(Table table) + { + var actual = this.ScenarioContext.Get(); + table.CompareToInstance(actual); + } + + [Then(@"預期得到回傳 Member\.FullName 結果為")] + public void Then預期得到回傳MemberFullName結果為(Table table) + { + var actual = this.ScenarioContext.Get().FullName; + table.CompareToInstance(actual); + } + + [Then(@"預期回傳內容為")] + public void Then預期回傳內容為(string expected) + { + var data = this.ScenarioContext.Get(); + var actual = JsonSerializer.Serialize(data); + JsonAssert.Equal(expected, actual,true); + } +} \ No newline at end of file diff --git a/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/Lab.JsonPathForTestCase.Test.csproj b/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/Lab.JsonPathForTestCase.Test.csproj new file mode 100644 index 00000000..ca3186ac --- /dev/null +++ b/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/Lab.JsonPathForTestCase.Test.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + + false + + latest + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/SpecFlowFeature1.feature b/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/SpecFlowFeature1.feature new file mode 100644 index 00000000..15fff3b2 --- /dev/null +++ b/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/SpecFlowFeature1.feature @@ -0,0 +1,33 @@ +Feature: SpecFlowFeature1 +Simple calculator for adding two numbers + + Scenario: 用 Table 驗證資料 + When 調用端發送 "Get" 請求至 "api/member" + Then 預期得到回傳 Member 結果為 + | Age | Birthday | + | 18 | 1/1/2000 12:00:00 AM +00:00 | + Then 預期得到回傳 Member.FullName 結果為 + | FirstName | LastName | + | John | Doe | + + Scenario: 用 JsonDiff 驗證資料 + When 調用端發送 "Get" 請求至 "api/member" + Then 預期回傳內容為 + """ + { + "Id": 1, + "Age": 18, + "Birthday": "2000-01-01T00:00:00+00:00", + "FullName": { + "FirstName": "John", + "LastName": "Doe" + } + } + """ + + Scenario: 用 JsonPath 驗證資料 + When 調用端發送 "Get" 請求至 "api/member" + Then 預期回傳內容中路徑 "$.Age" 的數值等於 "18" + Then 預期回傳內容中路徑 "$.Birthday" 的時間等於 "2000-01-01T00:00:00+00:00" + Then 預期回傳內容中路徑 "$.FullName.FirstName" 的字串等於 "John" + Then 預期回傳內容中路徑 "$.FullName.LastName" 的字串等於 "Doe" \ No newline at end of file diff --git a/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/UnitTest1.cs b/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/UnitTest1.cs new file mode 100644 index 00000000..32b5ed67 --- /dev/null +++ b/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.Test/UnitTest1.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Nodes; +using Json.Path; +using Xunit; + +namespace Lab.JsonPathForTestCase.Test; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + var json = """ + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + } + } + """; + + var instance = JsonNode.Parse(json); + var path = JsonPath.Parse("$.store.book[0].category"); + var result = path.Evaluate(instance); + + } +} \ No newline at end of file diff --git a/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.sln b/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.sln new file mode 100644 index 00000000..ba516ac3 --- /dev/null +++ b/Json/Lab.JsonPathForTestCase/Lab.JsonPathForTestCase.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.JsonPathForTestCase.Test", "Lab.JsonPathForTestCase.Test\Lab.JsonPathForTestCase.Test.csproj", "{25BC5C3D-55E4-40CB-AF4C-F3FE122F33BD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {25BC5C3D-55E4-40CB-AF4C-F3FE122F33BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25BC5C3D-55E4-40CB-AF4C-F3FE122F33BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25BC5C3D-55E4-40CB-AF4C-F3FE122F33BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25BC5C3D-55E4-40CB-AF4C-F3FE122F33BD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal 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/ModelValidation/net core model validation/Lab.ModelValidation.API/Controllers/FailureObjectResult.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/Controllers/FailureObjectResult.cs new file mode 100644 index 00000000..d21dd82d --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/Controllers/FailureObjectResult.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.ModelValidation.API.Controllers; + +public class FailureObjectResult : ObjectResult +{ + public FailureObjectResult(Failure error, int statusCode = StatusCodes.Status400BadRequest) + : base(error) + { + this.StatusCode = statusCode; + this.Value = error; + } +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/Controllers/GenericController.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/Controllers/GenericController.cs new file mode 100644 index 00000000..9faa6a08 --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/Controllers/GenericController.cs @@ -0,0 +1,37 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.ModelValidation.API.Controllers; + +public class GenericController : ControllerBase +{ + private static readonly Lazy> s_failureLookupLazy = new(() => new() + { + { FailureCode.InputInvalid, StatusCodes.Status400BadRequest }, + { FailureCode.MemberAlreadyExist, StatusCodes.Status400BadRequest }, + { FailureCode.MemberNotFound, StatusCodes.Status404NotFound }, + { FailureCode.DataNotFound, StatusCodes.Status404NotFound }, + { FailureCode.DataConcurrency, StatusCodes.Status429TooManyRequests }, + { FailureCode.ServerError, StatusCodes.Status500InternalServerError }, + { FailureCode.DbError, StatusCodes.Status500InternalServerError }, + { FailureCode.S3Error, StatusCodes.Status500InternalServerError }, + }); + + private static readonly Dictionary FailureLookup = s_failureLookupLazy.Value; + + [NonAction] + public FailureObjectResult GenericFailure(Failure failure) + { + if (string.IsNullOrWhiteSpace(failure.TraceId)) + { + failure.TraceId = Activity.Current?.Id ?? this.HttpContext.TraceIdentifier; + } + + if (FailureLookup.TryGetValue(failure.Code, out int statusCode)) + { + return new FailureObjectResult(failure, statusCode); + } + + return new FailureObjectResult(failure); + } +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/Controllers/MembersController.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/Controllers/MembersController.cs new file mode 100644 index 00000000..2a5d34dd --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/Controllers/MembersController.cs @@ -0,0 +1,47 @@ +using FluentValidation; +using Lab.ModelValidation.API.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.ModelValidation.API.Controllers; + +[ApiController] +[Route("[controller]")] +public class MembersController : GenericController +{ + private readonly ILogger _logger; + private readonly MemberService _memberService; + + public MembersController(ILogger logger, + MemberService memberService) + { + this._logger = logger; + this._memberService = memberService; + } + + // [ModelValidation()] + [HttpPost(Name = "CreateData")] + public async Task Post(CreateMemberRequest request, + CancellationToken cancel) + { + var createMemberResult = await this._memberService.CreateMemberAsync(request, cancel); + if (createMemberResult.Failure != null) + { + return this.GenericFailure(createMemberResult.Failure); + } + + return this.NoContent(); + } + + [HttpGet("{id}", Name = "GetData")] + public ActionResult Get(int id) + { + var service = new MemberServiceTemp(); + var (failure, _) = service.GetMember(id); + if (failure != null) + { + return this.GenericFailure(failure); + } + + return this.NoContent(); + } +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/Extensions/ValidationResultExtension.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/Extensions/ValidationResultExtension.cs new file mode 100644 index 00000000..938cbe06 --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/Extensions/ValidationResultExtension.cs @@ -0,0 +1,24 @@ +using FluentValidation.Results; + +namespace Lab.ModelValidation.API.Extensions; + +static class ValidationResultExtension +{ + public static Failure ToFailure(this ValidationResult validateResult) + { + if (validateResult.IsValid) + { + return null; + } + + var errors = validateResult.Errors + .ToDictionary(p => p.PropertyName, p => p.ErrorMessage); + var failure = new Failure() + { + Code = FailureCode.InputInvalid, + Message = "input invalid", + Data = errors, + }; + return failure; + } +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/Failure.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/Failure.cs new file mode 100644 index 00000000..892c247e --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/Failure.cs @@ -0,0 +1,24 @@ +namespace Lab.ModelValidation.API; + +public class Failure +{ + public Failure() + { + } + + public Failure(FailureCode code, string message) + { + this.Code = code; + this.Message = message; + } + + public FailureCode Code { get; init; } + + public string Message { get; init; } + + public object Data { get; init; } + + public string TraceId { get; set; } + + public List Details { get; init; } +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/FailureCode.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/FailureCode.cs new file mode 100644 index 00000000..e61f26df --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/FailureCode.cs @@ -0,0 +1,15 @@ +namespace Lab.ModelValidation.API; + +public enum FailureCode +{ + Unknown = 0, + InputInvalid = 1, + MemberNotFound, + MemberAlreadyExist, + ServerError, + DataConflict, + DataConcurrency, + DataNotFound, + DbError, + S3Error +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/Filters/ModelValidationAttribute.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/Filters/ModelValidationAttribute.cs new file mode 100644 index 00000000..230972c4 --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/Filters/ModelValidationAttribute.cs @@ -0,0 +1,70 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Lab.ModelValidation.API.Filters; + +public class ModelValidationAttribute : ActionFilterAttribute +{ + public override void OnActionExecuting(ActionExecutingContext actionContext) + { + if (actionContext.Result != null) + { + return; + } + + if (actionContext.ModelState.IsValid) + { + return; + } + + var traceId = Activity.Current?.Id ?? actionContext.HttpContext.TraceIdentifier; + + //處理 JSON Path + var jsonPathKeys = actionContext.ModelState.Keys.Where(e => e.StartsWith("$.")).ToList(); + if (jsonPathKeys.Count > 0) + { + var errorData = new Dictionary(); + foreach (var key in jsonPathKeys) + { + var normalizedKey = key.Substring(2); + foreach (var error in actionContext.ModelState[key].Errors) + { + if (error.Exception != null) + { + actionContext.ModelState.TryAddModelException(normalizedKey, error.Exception); + } + + actionContext.ModelState.TryAddModelError(normalizedKey, "The provided value is not valid."); + errorData.Add(normalizedKey, error.ErrorMessage); + } + + actionContext.ModelState.Remove(key); + } + + //複寫錯誤內容 + actionContext.Result = new BadRequestObjectResult(new Failure + { + Code = FailureCode.InputInvalid, + Message = "enum invalid", + Data = errorData, + TraceId = traceId + }); + return; + } + + var errors = actionContext.ModelState.ToDictionary( + p => p.Key, + p => p.Value.Errors.Select(e => e.ErrorMessage).ToList()); + + //複寫錯誤內容 + actionContext.Result = new BadRequestObjectResult(new Failure() + { + Code = FailureCode.InputInvalid, + Message = "input invalid", + Data = errors, + TraceId = traceId + }); + } +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/Lab.ModelValidation.API.csproj b/ModelValidation/net core model validation/Lab.ModelValidation.API/Lab.ModelValidation.API.csproj new file mode 100644 index 00000000..760b3f98 --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/Lab.ModelValidation.API.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/MemberService.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/MemberService.cs new file mode 100644 index 00000000..47140ae0 --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/MemberService.cs @@ -0,0 +1,28 @@ +using FluentValidation; +using Lab.ModelValidation.API.Extensions; +using Lab.ModelValidation.API.Models; + +namespace Lab.ModelValidation.API; + +public class MemberService +{ + private readonly IValidator _validator; + + public MemberService(IValidator validator) + { + this._validator = validator; + } + + public async Task<(Failure Failure, bool Data)> CreateMemberAsync(CreateMemberRequest request, + CancellationToken cancel = default) + { + var validateResult = await this._validator.ValidateAsync(request, cancel); + if (validateResult.IsValid == false) + { + var failure = validateResult.ToFailure(); + return (failure, false); + } + + return (null, true); + } +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/MemberServiceTemp.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/MemberServiceTemp.cs new file mode 100644 index 00000000..1df2518b --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/MemberServiceTemp.cs @@ -0,0 +1,53 @@ +namespace Lab.ModelValidation.API; + +public class MemberServiceTemp +{ + //一個方法有多種可能的 Failure + public (Failure Failure, bool Data) GetMember(int id) + { + if (id == 1) + { + return (new Failure + { + Code = FailureCode.MemberNotFound, + Message = "Member not found.", + }, true); + } + + if (id == 2) + { + return (new Failure + { + Code = FailureCode.MemberAlreadyExist, + Message = "Member already exist.", + }, true); + } + + if (id == 3) + { + return (new Failure + { + Code = FailureCode.DataConcurrency, + Message = "Data concurrency error.", + }, true); + } + + return (null, false); + } + + //具有多個 Detail 的 Failure + public (Failure Failure, bool Data) GetMember1() + { + var failure = new Failure() + { + Code = FailureCode.InputInvalid, + Message = "view detail errors", + Details = new List() + { + new(code: FailureCode.MemberNotFound, message: "Member not found."), + new(code: FailureCode.MemberAlreadyExist, message: "Member already exist.") + } + }; + return (failure, false); + } +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/Models/CreateMemberRequest.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/Models/CreateMemberRequest.cs new file mode 100644 index 00000000..cc8ba166 --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/Models/CreateMemberRequest.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using FluentValidation; + +namespace Lab.ModelValidation.API.Models; + +public class CreateMemberRequestValidator : AbstractValidator +{ + public CreateMemberRequestValidator() + { + this.RuleFor(p => p.Name).NotNull().NotEmpty(); + this.RuleFor(p => p.Age).LessThanOrEqualTo(18).GreaterThan(200); + this.RuleFor(x => x.Type) + .IsInEnum() + .WithMessage("Type is not valid"); + } +} + +public enum MemberType +{ + None, + Member, + Vip, +} + +public class CreateMemberRequest +{ + [Required] + public string Name { get; set; } + + [Range(18, 200)] + public int Age { get; set; } + + [Required] + public MemberType Type { get; set; } +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/Models/GetMemberResult.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/Models/GetMemberResult.cs new file mode 100644 index 00000000..796f8aac --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/Models/GetMemberResult.cs @@ -0,0 +1,12 @@ +using Lab.ModelValidation.API.Controllers; + +namespace Lab.ModelValidation.API.Models; + +public class GetMemberResult +{ + public Guid Id { get; set; } + + public string Name { get; set; } + + public int Age { get; set; } +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/Program.cs b/ModelValidation/net core model validation/Lab.ModelValidation.API/Program.cs new file mode 100644 index 00000000..bcdf5db7 --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/Program.cs @@ -0,0 +1,124 @@ +using System.Diagnostics; +using System.Runtime.InteropServices.JavaScript; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using FluentValidation; +using FluentValidation.AspNetCore; +using Lab.ModelValidation.API; +using Lab.ModelValidation.API.Filters; +using Lab.ModelValidation.API.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services + .AddControllers(p => + { + // p.ModelValidatorProviders.Clear(); + }) + .AddFluentValidation(p => p.RegisterValidatorsFromAssemblyContaining()) + .AddJsonOptions(options => + { + options.JsonSerializerOptions.MaxDepth = 10; + options.JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + options.AllowInputFormatterExceptionMessages = true; + }) + ; +builder.Services.Configure(options => +{ + //停用 Model State Invalid Filter + options.SuppressModelStateInvalidFilter = true; + + // options.InvalidModelStateResponseFactory = actionContext => ValidationErrorHandler(options, actionContext); +}); + +//隨便挑一個 Validator 物件來註冊 FluentValidation +// builder.Services.AddValidatorsFromAssemblyContaining(); +builder.Services.AddScoped(); + +// 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(); + +IActionResult ValidationErrorHandler(ApiBehaviorOptions apiBehaviorOptions, ActionContext actionContext) +{ + var originalFactory = apiBehaviorOptions.InvalidModelStateResponseFactory; + if (actionContext.ModelState.IsValid) + { + return originalFactory(actionContext); + } + + var traceId = Activity.Current?.Id ?? actionContext.HttpContext.TraceIdentifier; + + //處理 JSON Path + var jsonPathKeys = actionContext.ModelState.Keys.Where(e => e.StartsWith("$.")).ToList(); + if (jsonPathKeys.Count > 0) + { + var errorData = new Dictionary(); + foreach (var key in jsonPathKeys) + { + var normalizedKey = key.Substring(2); + foreach (var error in actionContext.ModelState[key].Errors) + { + if (error.Exception != null) + { + actionContext.ModelState.TryAddModelException(normalizedKey, error.Exception); + } + + actionContext.ModelState.TryAddModelError(normalizedKey, "The provided value is not valid."); + errorData.Add(normalizedKey, error.ErrorMessage); + } + + actionContext.ModelState.Remove(key); + } + + //複寫錯誤內容 + return new BadRequestObjectResult(new Failure + { + Code = FailureCode.InputInvalid, + Message = "enum invalid", + Data = errorData, + TraceId = traceId + }); + } + + var errors = actionContext.ModelState.ToDictionary( + p => p.Key, + p => p.Value.Errors.Select(e => e.ErrorMessage).ToList()); + + //複寫錯誤內容 + return new BadRequestObjectResult(new Failure() + { + Code = FailureCode.InputInvalid, + Message = "input invalid", + Data = errors, + TraceId = traceId + }); +} \ No newline at end of file diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/appsettings.Development.json b/ModelValidation/net core model validation/Lab.ModelValidation.API/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ModelValidation/net core model validation/Lab.ModelValidation.API/appsettings.json b/ModelValidation/net core model validation/Lab.ModelValidation.API/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/ModelValidation/net core model validation/Lab.ModelValidation.API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ModelValidation/net core model validation/net core model validation.sln b/ModelValidation/net core model validation/net core model validation.sln new file mode 100644 index 00000000..f5e28a4b --- /dev/null +++ b/ModelValidation/net core model validation/net core model validation.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ModelValidation.API", "Lab.ModelValidation.API\Lab.ModelValidation.API.csproj", "{010C4B0F-E5E4-4AB8-A3D4-80DF5A5DDB9A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {010C4B0F-E5E4-4AB8-A3D4-80DF5A5DDB9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {010C4B0F-E5E4-4AB8-A3D4-80DF5A5DDB9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {010C4B0F-E5E4-4AB8-A3D4-80DF5A5DDB9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {010C4B0F-E5E4-4AB8-A3D4-80DF5A5DDB9A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.TestProject/Lab.MongoDB.CRUD.TestProject.csproj b/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.TestProject/Lab.MongoDB.CRUD.TestProject.csproj new file mode 100644 index 00000000..55c74004 --- /dev/null +++ b/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.TestProject/Lab.MongoDB.CRUD.TestProject.csproj @@ -0,0 +1,24 @@ + + + + net7.0 + enable + enable + + false + + Linux + + + + + + + + + + + + + + diff --git a/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.TestProject/UnitTest1.cs b/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.TestProject/UnitTest1.cs new file mode 100644 index 00000000..b964f4df --- /dev/null +++ b/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.TestProject/UnitTest1.cs @@ -0,0 +1,242 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Testcontainers.MongoDb; + +namespace Lab.MongoDB.CRUD.TestProject; + +[TestClass] +public class UnitTest1 +{ + private static MongoDbContainer MongoDbContainer; + private static MongoClient MongoClient; + private readonly string TestData = "出發吧,讓我們航向偉大的航道"; + + [ClassInitialize] + public static async Task ClassInitialize(TestContext context) + { + // MongoDbContainer = new MongoDbBuilder() + // .WithPortBinding(27017, true) + // .Build(); + // await MongoDbContainer.StartAsync(); + // var mongoClientSettings = MongoClientSettings.FromConnectionString(MongoDbContainer.GetConnectionString()); + var mongoClientSettings = new MongoClientSettings() + { + Server = new MongoServerAddress("localhost", 27017), + }; + + MongoClient = new MongoClient(mongoClientSettings); + } + + [ClassCleanup] + public static async Task ClassCleanup() + { + // await MongoDbContainer.DisposeAsync(); + } + + [TestCleanup] + public void TestCleanup() + { + //復原資料 + var mongoCollection = MongoClient.GetDatabase("example").GetCollection("product"); + var filter = Builders.Filter + .Eq(r => r.Remark, this.TestData); + var data = mongoCollection.DeleteMany(filter); + } + + [TestMethod] + public async Task 新增一筆資料() + { + var mongoCollection = MongoClient.GetDatabase("example").GetCollection("product"); + var expected = new Product + { + Id = "1", + Name = "TV", + Price = 33.11m, + Remark = this.TestData + }; + + //新增一筆資料 + await mongoCollection.InsertOneAsync(expected); + mongoCollection.InsertOne(expected,new InsertOneOptions + { + BypassDocumentValidation = null, + Comment = null + }); + //驗證 + var actual = await mongoCollection.AsQueryable().FirstOrDefaultAsync(p => p.Id == "1"); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public async Task 更新一筆資料() + { + var mongoCollection = MongoClient.GetDatabase("example").GetCollection("product"); + var expected = new Product + { + Id = "1", + Name = "TV", + Price = 33.11m, + Remark = this.TestData + }; + + //產生資料 + var products = this.GenerateProducts(); + await mongoCollection.InsertManyAsync(products); + + var filter = Builders.Filter + .Eq(restaurant => restaurant.Id, "1"); + + var update = Builders.Update + .Set(restaurant => restaurant.Name, "TV"); + + //更新資料 + await mongoCollection.UpdateOneAsync(filter, update); + + //驗證 + var actual = await mongoCollection.AsQueryable().FirstOrDefaultAsync(p => p.Id == "1"); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public async Task 刪除資料() + { + var mongoCollection = MongoClient.GetDatabase("example").GetCollection("product"); + + //產生資料 + var products = this.GenerateProducts(); + await mongoCollection.InsertManyAsync(products); + + var filter = Builders.Filter + .Eq(restaurant => restaurant.Id, "1"); + + //更新資料 + await mongoCollection.DeleteOneAsync(filter); + + //驗證 + var actual = await mongoCollection.AsQueryable().FirstOrDefaultAsync(p => p.Id == "1"); + + Assert.AreEqual(null, actual); + } + + + [TestMethod] + public async Task 查詢() + { + var mongoCollection = MongoClient.GetDatabase("example").GetCollection("product"); + var expected = new Product + { + Id = "1", + Name = "Air jordan 11", + Price = 33.11m, + Remark = this.TestData + }; + + //產生資料 + var products = this.GenerateProducts(); + await mongoCollection.InsertManyAsync(products); + + //查詢1 + var filter = Builders.Filter.Eq(x => x.Id, "1"); + var find = await mongoCollection.FindAsync(filter); + var data1 = await find.FirstOrDefaultAsync(); + + //驗證 + Assert.AreEqual(expected, data1); + + //查詢2 + var data2 = await mongoCollection.AsQueryable().FirstOrDefaultAsync(p => p.Id == "1"); + + //驗證 + Assert.AreEqual(expected, data2); + } + + + [TestMethod] + public async Task AA() + { + var database = MongoClient.GetDatabase("example"); + var mongoCollection = database.GetCollection("counters"); + var sequenceGenerator = new SequenceGenerator (database); + var sequenceValue = sequenceGenerator.GetNextSequenceValue("_id"); + } + + private List GenerateProducts() + { + var products = new List() + { + new() + { + // Id = "1", + Name = "Air jordan 11", + Price = 33.11m, + Remark = this.TestData + }, + new() + { + // Id = "1", + Name = "Air jordan 12", + Price = 33.12m, + Remark = this.TestData + }, + new() + { + // Id = "3", + Name = "Air jordan 13", + Price = 33.13m, + Remark = this.TestData + } + }; + return products; + } + + public record Product + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + + // [BsonElement("Id1")] + public string Id { get; init; } + + public string Name { get; init; } + + public decimal Price { get; init; } + + public string Remark { get; init; } + } + + public class SequenceGenerator + { + private IMongoCollection countersCollection; + + public SequenceGenerator(IMongoDatabase database) + { + countersCollection = database.GetCollection("counters"); + } + + public int GetNextSequenceValue(string sequenceName) + { + var filter = Builders.Filter.Eq("_id", sequenceName); + var update = Builders.Update.Inc("sequence_value", 1); + + var result = countersCollection.FindOneAndUpdate(filter, update); + + if (result == null) + { + // If the document doesn't exist, create it with initial value 1 + var newCounterDocument = new BsonDocument + { + { "_id", sequenceName }, + { "sequence_value", 1 } + }; + countersCollection.InsertOne(newCounterDocument); + return 1; + } + + return result["sequence_value"].AsInt32; + } + } + +} \ No newline at end of file diff --git a/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.TestProject/Usings.cs b/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.TestProject/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.TestProject/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.sln b/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.sln new file mode 100644 index 00000000..90146883 --- /dev/null +++ b/MongoDB/Lab.MongoDB.CRUD/Lab.MongoDB.CRUD.sln @@ -0,0 +1,21 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.MongoDB.CRUD.TestProject", "Lab.MongoDB.CRUD.TestProject\Lab.MongoDB.CRUD.TestProject.csproj", "{58427830-E529-4C0F-A0E3-0D3E14BB81DC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C09F0E2A-1C9B-440C-BAF0-1E9C365E6362}" + 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 + {58427830-E529-4C0F-A0E3-0D3E14BB81DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58427830-E529-4C0F-A0E3-0D3E14BB81DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58427830-E529-4C0F-A0E3-0D3E14BB81DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58427830-E529-4C0F-A0E3-0D3E14BB81DC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/MongoDB/Lab.MongoDB.CRUD/docker-compose.yml b/MongoDB/Lab.MongoDB.CRUD/docker-compose.yml new file mode 100644 index 00000000..b7ca4a4a --- /dev/null +++ b/MongoDB/Lab.MongoDB.CRUD/docker-compose.yml @@ -0,0 +1,21 @@ +version: "3.8" + +services: + mongo: + image: mongo + container_name: mongo_test + ports: + - 27017:27017 +# environment: +# MONGO_INITDB_ROOT_USERNAME: root +# MONGO_INITDB_ROOT_PASSWORD: example + + mongo-express: + image: mongo-express + container_name: mongo_express_test + ports: + - 8081:8081 +# environment: +# ME_CONFIG_MONGODB_ADMINUSERNAME: root +# ME_CONFIG_MONGODB_ADMINPASSWORD: example + \ No newline at end of file diff --git a/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/EntityModel/Employee.cs b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/EntityModel/Employee.cs new file mode 100644 index 00000000..d03aed80 --- /dev/null +++ b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/EntityModel/Employee.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.ODBC.PG.Test.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; } + } +} \ No newline at end of file diff --git a/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/EntityModel/EmployeeDbContext.cs b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..f5e08b73 --- /dev/null +++ b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; + +namespace Lab.ODBC.PG.Test.EntityModel +{ + public class EmployeeDbContext : DbContext + { + public virtual DbSet Employees { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(p => + { + p.HasKey(e => e.Id); + p.HasIndex(e => e.SequenceId) + .IsUnique(); + p.Property(p => p.Remark).IsRequired(false); + }); + } + } +} \ No newline at end of file diff --git a/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/EntityModel/EmployeeDbContextContextFactory.cs b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/EntityModel/EmployeeDbContextContextFactory.cs new file mode 100644 index 00000000..26f41041 --- /dev/null +++ b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/EntityModel/EmployeeDbContextContextFactory.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Lab.ODBC.PG.Test.EntityModel; + +public class EmployeeDbContextContextFactory : IDesignTimeDbContextFactory +{ + public EmployeeDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder + .UseNpgsql("Host=localhost;Port=5432;Database=employee;Username=postgres;Password=guest") + ; + + return new EmployeeDbContext(optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/Initialize.cs b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/Initialize.cs new file mode 100644 index 00000000..743662bb --- /dev/null +++ b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/Initialize.cs @@ -0,0 +1,67 @@ +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using Lab.ODBC.PG.Test.EntityModel; +using Microsoft.EntityFrameworkCore; +using Npgsql; + +namespace Lab.ODBC.PG.Test; + +[TestClass] +public static class Initialize +{ + static IContainer? PostgreSqlContainer; + + [AssemblyInitialize] + public static void AssemblyInitialize(TestContext context) + { + Console.WriteLine("AssemblyInitialize"); + PostgreSqlContainer = CreatePostgreSQLContainer(); + PostgreSqlContainer.StartAsync().GetAwaiter().GetResult(); + + InsertTestData(); + } + + private static void InsertTestData() + { + var connectionString = "Host=localhost;Port=5432;Database=employee;Username=postgres;Password=postgres"; + var dbContextOptions = new DbContextOptionsBuilder() + .UseNpgsql(connectionString) + .Options; + using var dbContext = new EmployeeDbContext(dbContextOptions); + dbContext.Database.EnsureCreated(); + dbContext.Employees.Add(new Employee + { + Id = Guid.NewGuid(), + Name = "yao", + Age = 18, + Remark = null, + CreateAt = DateTime.UtcNow, + CreateBy = "yao" + }); + dbContext.SaveChanges(); + } + + [AssemblyCleanup] + public static void AssemblyCleanup() + { + Console.WriteLine("AssemblyCleanup"); + PostgreSqlContainer.StopAsync().GetAwaiter().GetResult(); + PostgreSqlContainer.DisposeAsync().GetAwaiter().GetResult(); + } + + private static IContainer CreatePostgreSQLContainer() + { + var waitStrategy = Wait.ForUnixContainer().UntilCommandIsCompleted("pg_isready"); + var container = new ContainerBuilder() + .WithImage("postgres:12-alpine") + .WithName("postgres.12") + .WithPortBinding(5432) + .WithWaitStrategy(waitStrategy) + .WithEnvironment("POSTGRES_USER", "postgres") + .WithEnvironment("POSTGRES_PASSWORD", "postgres") + .WithEnvironment("POSTGRES_DB", "employee") + .Build(); + + return container; + } +} \ No newline at end of file diff --git a/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/Lab.ODBC.PG.Test.csproj b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/Lab.ODBC.PG.Test.csproj new file mode 100644 index 00000000..f67dea20 --- /dev/null +++ b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/Lab.ODBC.PG.Test.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/UnitTest1.cs b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/UnitTest1.cs new file mode 100644 index 00000000..20b9a417 --- /dev/null +++ b/ODBC/Lab.ODBC/Lab.ODBC.PG.Test/UnitTest1.cs @@ -0,0 +1,87 @@ +using System.Data.Odbc; +using Dapper; +using Lab.ODBC.PG.Test.EntityModel; +using Microsoft.EntityFrameworkCore; + +namespace Lab.ODBC.PG.Test; + +[TestClass] +public class PostgreSqlOdbcTest +{ + [TestMethod] + public async Task ReadForAdoNet() + { + var connectionString = + "Driver={PostgreSQL Unicode};Server=localhost;Port=5432;Database=employee;Uid=postgres;Pwd=postgres;"; + await using var connection = new OdbcConnection(connectionString); + await using var command = new OdbcCommand(); + await connection.OpenAsync(); + command.Connection = connection; + command.CommandText = @"select * from ""Employee"""; + await using var reader = await command.ExecuteReaderAsync(); + + while (true) + { + var hasData = await reader.ReadAsync(); + if (hasData == false) + { + break; + } + + for (var i = 0; i < reader.FieldCount; i++) + { + var name = reader.GetName(i); + var value = reader.GetValue(i); + Console.WriteLine($"{name}: {value}"); + } + } + + await connection.CloseAsync(); + } + + [TestMethod] + public async Task ReadForDapper() + { + var connectionString = + "Driver={PostgreSQL Unicode};Server=localhost;Port=5432;Database=employee;Uid=postgres;Pwd=postgres;"; + await using var connection = new OdbcConnection(connectionString); + var sql = @"select * from ""Employee"""; + var data = connection.Query(sql).ToList(); + await connection.CloseAsync(); + } + + [TestMethod] + public async Task ReadForDsn() + { + var dsn = "PostgreSQL"; + var connectionString = $"DSN={dsn}"; + await using var connection = new OdbcConnection(connectionString); + var sql = @"select * from ""Employee"""; + var data = connection.Query(sql).ToList(); + await connection.CloseAsync(); + } + + [TestMethod] + public async Task ReadForEF() + { + await using var dbContext = CreateDbContext(); + + var employees = await dbContext.Employees.AsTracking().ToListAsync(); + } + + private static EmployeeDbContext CreateDbContext() + { + // var connectionString = + // "Driver={PostgreSQL Unicode};Server=localhost;Port=5432;Database=employee;Uid=postgres;Pwd=postgres"; + var connectionString = + "Host=localhost;Port=5432;Database=employee;Username=postgres;Password=postgres"; + var builder = new DbContextOptionsBuilder(); + builder.UseNpgsql(connectionString); + var dbContextOptions = builder + .Options; + + var dbContext = new EmployeeDbContext(dbContextOptions); + dbContext.Database.SetConnectionString(connectionString); + return dbContext; + } +} \ No newline at end of file diff --git a/ODBC/Lab.ODBC/Lab.ODBC.sln b/ODBC/Lab.ODBC/Lab.ODBC.sln new file mode 100644 index 00000000..b49a3e9f --- /dev/null +++ b/ODBC/Lab.ODBC/Lab.ODBC.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ODBC.PG.Test", "Lab.ODBC.PG.Test\Lab.ODBC.PG.Test.csproj", "{B6499068-9D46-4967-8FEE-84448692EAC7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B6499068-9D46-4967-8FEE-84448692EAC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6499068-9D46-4967-8FEE-84448692EAC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6499068-9D46-4967-8FEE-84448692EAC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6499068-9D46-4967-8FEE-84448692EAC7}.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.Sharding/.dockerignore b/ORM/EFCore/Lab.Sharding/.dockerignore new file mode 100644 index 00000000..53932437 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/.dockerignore @@ -0,0 +1,11 @@ +k8s +README.md +.gitignore +.vscode +.gitlab-ci.yml +ci.custom.script.sh + +**/bin/ +**/obj/ + +node_modules diff --git a/ORM/EFCore/Lab.Sharding/.editorconfig b/ORM/EFCore/Lab.Sharding/.editorconfig new file mode 100644 index 00000000..3ca9e23d --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/.editorconfig @@ -0,0 +1,323 @@ +## C# formatting options: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/csharp-formatting-options + +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +#### Core EditorConfig Options #### +[*] + +# Indentation and spacing +end_of_line = lf +indent_size = 4 +indent_style = tab +tab_width = 4 +charset = utf-8 + + +# C# files +[*.cs] + +### C# formatting options +## New-line options +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +## Indentation options +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false + +## Spacing options +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false + +## Wrap options +# Reference: https://learn.microsoft.com/zh-tw/dotnet/fundamentals/code-analysis/style-rules/ide0011 +csharp_prefer_braces = true:silent +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true + +### .NET formatting options + +## Using directive options +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = true + +## Dotnet namespace options +dotnet_style_namespace_match_folder = true + + +#### .NET Coding Conventions #### + +# this. and Me. preferences +dotnet_style_qualification_for_method = true + +# var suggestion options +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion + +# naming style options +dotnet_naming_rule.constants_rule.severity = warning +dotnet_naming_rule.constants_rule.style = all_upper_style +dotnet_naming_rule.constants_rule.symbols = constants_symbols +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = s_lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = s_lower_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_rule.public_fields_rule.severity = warning +dotnet_naming_rule.public_fields_rule.style = upper_camel_case_style +dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols +dotnet_naming_rule.static_readonly_rule.severity = warning +dotnet_naming_rule.static_readonly_rule.style = all_upper_style +dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols +dotnet_naming_rule.type_parameters_rule.severity = warning +dotnet_naming_rule.type_parameters_rule.style = upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols +dotnet_naming_rule.unity_serialized_field_rule.severity = warning +dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols +dotnet_naming_style.all_upper_style.capitalization = all_upper +dotnet_naming_style.all_upper_style.word_separator = _ +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.s_lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.s_lower_camel_case_style.required_prefix = s_ +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.constants_symbols.applicable_kinds = field +dotnet_naming_symbols.constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static, readonly +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = true:suggestion +dotnet_style_qualification_for_field = true:suggestion +dotnet_style_qualification_for_method = true:suggestion +dotnet_style_qualification_for_property = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = true +dotnet_style_qualification_for_property = true +dotnet_style_qualification_for_method = true +dotnet_style_qualification_for_event = true + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Whitespace options +dotnet_style_allow_multiple_blank_lines_experimental = false + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Non-private readonly fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case + +# Constants are PascalCase + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = pascal_case + +# Static fields are camelCase and start with s_ +dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_field_style.capitalization = camel_case +dotnet_naming_style.static_field_style.required_prefix = s_ + +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +# By default, name items with PascalCase +dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.all_members.applicable_kinds = * + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# RS0016: Only enable if API files are present +dotnet_public_api_analyzer.require_api_files = true + +#### Diagnostic configuration #### + +# CA1000: Do not declare static members on generic types +dotnet_diagnostic.CA1000.severity = warning + +# IDE0055: Fix formatting +# Reference: https://learn.microsoft.com/zh-tw/dotnet/fundamentals/code-analysis/style-rules/ide0055 +dotnet_diagnostic.IDE0055.severity = warning + +# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}' +dotnet_diagnostic.rs2008.severity = none + +# IDE0035: Remove unreachable code +dotnet_diagnostic.ide0035.severity = warning + +# IDE0036: Order modifiers +dotnet_diagnostic.ide0036.severity = warning + +# IDE0043: Format string contains invalid placeholder +dotnet_diagnostic.ide0043.severity = warning + +# IDE0044: Make field readonly +dotnet_diagnostic.ide0044.severity = warning + +# dotnet_style_allow_multiple_blank_lines_experimental +dotnet_diagnostic.ide2000.severity = warning + +# csharp_style_allow_embedded_statements_on_same_line_experimental +dotnet_diagnostic.ide2001.severity = warning + +# csharp_style_allow_blank_lines_between_consecutive_braces_experimental +dotnet_diagnostic.ide2002.severity = warning + +# dotnet_style_allow_statement_immediately_after_block_experimental +dotnet_diagnostic.ide2003.severity = warning + +# csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental +dotnet_diagnostic.ide2004.severity = warning + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline:warning + +# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201 +dotnet_diagnostic.ide0011.severity = warning + +# IDE0040: Add accessibility modifiers +dotnet_diagnostic.ide0040.severity = warning + +# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings? +# IDE0051: Remove unused private member +dotnet_diagnostic.ide0051.severity = warning + +# IDE0052: Remove unread private member +dotnet_diagnostic.ide0052.severity = warning + +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.ide0059.severity = warning + +# IDE0060: Remove unused parameter +dotnet_diagnostic.ide0060.severity = warning + +# CA1012: Abstract types should not have public constructors +dotnet_diagnostic.ca1012.severity = warning + +# CA1822: Make member static +dotnet_diagnostic.ca1822.severity = warning + +[{*.yaml,*.yml}] +indent_size = 2 +tab_width = 2 \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/.gitignore b/ORM/EFCore/Lab.Sharding/.gitignore new file mode 100644 index 00000000..e6796067 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/.gitignore @@ -0,0 +1,347 @@ +## 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 +secret/ +logs/ + +.DS_Store +*.zip + diff --git a/ORM/EFCore/Lab.Sharding/Taskfile.yml b/ORM/EFCore/Lab.Sharding/Taskfile.yml new file mode 100644 index 00000000..2b747cfe --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/Taskfile.yml @@ -0,0 +1,65 @@ +# Taskfile.yml + +version: "3" + +dotenv: [ "env/local.env" ] + +tasks: + ## Develop --------------------------------------------------- + dev-init: + desc: Init development environment + cmds: + - task: redis-start + + redis-start: + desc: start redis 5.X version + cmds: + - docker-compose up -d redis + + redis-admin-start: + desc: admin ui to manage redis + cmds: + - docker-compose up -d redis-admin + + ef-codegen: + desc: Init development environment + cmds: + - task: ef-codegen-member + + ef-codegen-member: + desc: EF Core 反向工程產生 MemberDbContext EF Entities + dir: "src/be/Lab.Sharding.DB" + cmds: + - dotnet ef dbcontext scaffold "$SYS_DATABASE_CONNECTION_STRING" Microsoft.EntityFrameworkCore.SqlServer -o AutoGenerated/Entities -c MemberDbContext --context-dir AutoGenerated/ -n Lab.Sharding.DB -t Member --force --no-onconfiguring --use-database-names + - dotnet ef dbcontext scaffold "$SYS_DATABASE_CONNECTION_STRING" Microsoft.EntityFrameworkCore.SqlServer -o AutoGenerated/Entities -c Member1DbContext --context-dir AutoGenerated/ -n Lab.Sharding.DB -t Member --force --no-onconfiguring --use-database-names + + codegen-api: + desc: codegen client and server + cmds: + - task: codegen-api-client + - task: codegen-api-server + codegen-api-client: + desc: codegen client + cmds: + - refitter ./doc/openapi.yml --namespace "Lab.Sharding.Contract" --output ./src/be/Lab.Sharding.Contract/AutoGenerated/JobClient.cs --use-api-response --no-operation-headers --no-auto-generated-header + codegen-api-server: + desc: codegen server + cmds: + - nswag openapi2cscontroller /input:./doc/openapi.yml /classname:{controller} /namespace:Lab.Sharding.WebAPI.Contract /output:./src/be/Lab.Sharding.WebAPI/Contract/AutoGenerated/ContractControllers.cs /jsonLibrary:SystemTextJson /useCancellationToken:true /useActionResultType:true /operationGenerationMode:MultipleClientsFromFirstTagAndOperationId /controllerBaseClass:Microsoft.AspNetCore.Mvc.ControllerBase /excludedParameterNames:x-idempotency-key,x-api-key + + codegen-api-doc: + desc: codegen api doc + 安裝 Redocly CLI + npm install -g @redocly/openapi-cli + dir: "./doc" + cmds: + - redocly build-docs ./openapi.yml --output ./openapi.html +# - redocly build-docs ./doc/openapi.yml --output ./doc/openapi.html +# - redocly preview-docs ./doc/openapi.yml + #- redocly bundle ./doc/openapi.yml --output ./doc/openapi-bundled.yml + #- redocly preview-docs ./doc/openapi-bundled.yml + codegen-api-preview: + desc: codegen api doc + dir: "./doc" + cmds: + - redocly preview-docs ./openapi.yml diff --git a/ORM/EFCore/Lab.Sharding/docker-compose.yml b/ORM/EFCore/Lab.Sharding/docker-compose.yml new file mode 100644 index 00000000..1826da2b --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/docker-compose.yml @@ -0,0 +1,31 @@ +services: + seq: + image: datalust/seq:latest + ports: + - "5341:5341" + environment: + - ACCEPT_EULA=Y + redis: + image: redis:5.0.12-alpine + #command: [ "redis-server", "--bind", "redis", "--port", "6379" ] + ports: + - 6379:6379 + + # 連不上的話 host 打 redis:6379 + redis-admin: + image: marian/rebrow + ports: + - 8006:5001 + depends_on: + - redis + + sql2019: + image: 'mcr.microsoft.com/mssql/server:2019-latest' + hostname: 'sql2019' + container_name: 'sql2019' + ports: + - 1433:1433 + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=pass@w0rd1~ + - MSSQL_PID=Express diff --git a/ORM/EFCore/Lab.Sharding/env/local.env b/ORM/EFCore/Lab.Sharding/env/local.env new file mode 100644 index 00000000..49a50eca --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/env/local.env @@ -0,0 +1,5 @@ +#sql server connection string +SYS_DATABASE_CONNECTION_STRING=Server=localhost;Database=Member01;User Id=SA;Password=pass@w0rd1~;TrustServerCertificate=True +SYS_DATABASE_CONNECTION_STRING=Server=localhost;Database=Member01;User Id=SA;Password=pass@w0rd1~;TrustServerCertificate=True +SYS_REDIS_URL=localhost:6379 +EXTERNAL_API=http://localhost:5000/api diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/AutoGenerated/JobClient.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/AutoGenerated/JobClient.cs new file mode 100644 index 00000000..982dfff5 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/AutoGenerated/JobClient.cs @@ -0,0 +1,193 @@ +using System.Text.Json.Serialization; +using Refit; + +namespace Lab.Sharding.Contract.AutoGenerated; +#nullable enable annotations + +[System.CodeDom.Compiler.GeneratedCode("Refitter", "1.4.0.0")] +public partial interface IJobBank1111JobWebAPI +{ + /// + /// A representing the instance containing the result: + /// + /// + /// Status + /// Description + /// + /// + /// 200 + /// OK + /// + /// + /// 400 + /// Bad Request + /// + /// + /// + [Headers("Accept: text/plain, application/json, text/json")] + [Get("/api/v1/tagscursor")] + Task> GetTagsCursor(); + + /// + /// A representing the instance containing the result: + /// + /// + /// Status + /// Description + /// + /// + /// 200 + /// OK + /// + /// + /// + [Headers("Accept: text/plain, application/json, text/json")] + [Get("/api/v1/memberscursor")] + Task> GetMembersCursor(); + + /// + /// A representing the instance containing the result: + /// + /// + /// Status + /// Description + /// + /// + /// 200 + /// OK + /// + /// + /// + [Headers("Accept: text/plain, application/json, text/json")] + [Get("/api/v1/membersoffset")] + Task> GetMemberOffset(); + + /// + /// A representing the instance containing the result: + /// + /// + /// Status + /// Description + /// + /// + /// 200 + /// OK + /// + /// + /// + [Post("/api/v2/members")] + Task InsertMember2([Body] InsertMemberRequest body); + + /// + /// A representing the instance containing the result: + /// + /// + /// Status + /// Description + /// + /// + /// 200 + /// OK + /// + /// + /// + [Post("/api/v1/members")] + Task InsertMember1([Body] InsertMemberRequest body); + + +} + +//---------------------- +// +// Generated using the NSwag toolchain v14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.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 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" +#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" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" +#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" +#pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." + +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] +public partial class GetMemberResponse +{ + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("age")] + public int? Age { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonPropertyName("sequenceId")] + public long? SequenceId { get; set; } + +} + +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] +public partial class GetMemberResponseCursorPaginatedList +{ + + [JsonPropertyName("items")] + public ICollection Items { get; set; } + + [JsonPropertyName("nextPageToken")] + public string NextPageToken { get; set; } + + [JsonPropertyName("nextPreviousToken")] + public string NextPreviousToken { get; set; } + +} + +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] +public partial class InsertMemberRequest +{ + + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("age")] + public int Age { get; set; } + +} + +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] +public partial class Failure +{ + + [JsonPropertyName("code")] + public string Code { get; set; } + + [JsonPropertyName("reason")] + public string Reason { get; set; } + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 +#pragma warning restore 8604 +#pragma warning restore 8625 \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/Lab.Sharding.Contract.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/Lab.Sharding.Contract.csproj new file mode 100644 index 00000000..6b1fec70 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Contract/Lab.Sharding.Contract.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Entities/Member.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Entities/Member.cs new file mode 100644 index 00000000..21ed1d44 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Entities/Member.cs @@ -0,0 +1,22 @@ +namespace Lab.Sharding.DB.AutoGenerated.Entities; + +public partial class Member +{ + public string Id { get; set; } = null!; + + public string? Name { get; set; } + + public int? Age { get; set; } + + public long SequenceId { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string? CreatedBy { get; set; } + + public DateTimeOffset? ChangedAt { get; set; } + + public string? ChangedBy { get; set; } + + public string? Email { get; set; } +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member1DbContext.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member1DbContext.cs new file mode 100644 index 00000000..f23f7ce8 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member1DbContext.cs @@ -0,0 +1,39 @@ +using Lab.Sharding.DB.AutoGenerated.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Lab.Sharding.DB.AutoGenerated; + +public partial class Member1DbContext : DbContext +{ + public Member1DbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Members { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("Member_pk"); + + entity.ToTable("Member"); + + entity.Property(e => e.Id) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.ChangedBy).HasMaxLength(20); + entity.Property(e => e.CreatedBy).HasMaxLength(50); + entity.Property(e => e.Email) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.Name).HasMaxLength(20); + entity.Property(e => e.SequenceId).ValueGeneratedOnAdd(); + }); + + this.OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member2DbContext.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member2DbContext.cs new file mode 100644 index 00000000..091b6d55 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/Member2DbContext.cs @@ -0,0 +1,39 @@ +using Lab.Sharding.DB.AutoGenerated.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Lab.Sharding.DB.AutoGenerated; + +public partial class Member2DbContext : DbContext +{ + public Member2DbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Members { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("Member_pk"); + + entity.ToTable("Member"); + + entity.Property(e => e.Id) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.ChangedBy).HasMaxLength(20); + entity.Property(e => e.CreatedBy).HasMaxLength(50); + entity.Property(e => e.Email) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.Name).HasMaxLength(20); + entity.Property(e => e.SequenceId).ValueGeneratedOnAdd(); + }); + + this.OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/MemberDbContext.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/MemberDbContext.cs new file mode 100644 index 00000000..4b7faba6 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/AutoGenerated/MemberDbContext.cs @@ -0,0 +1,39 @@ +using Lab.Sharding.DB.AutoGenerated.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Lab.Sharding.DB.AutoGenerated; + +public partial class MemberDbContext : DbContext +{ + public MemberDbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Members { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("Member_pk"); + + entity.ToTable("Member"); + + entity.Property(e => e.Id) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.ChangedBy).HasMaxLength(20); + entity.Property(e => e.CreatedBy).HasMaxLength(50); + entity.Property(e => e.Email) + .HasMaxLength(50) + .IsUnicode(false); + entity.Property(e => e.Name).HasMaxLength(20); + entity.Property(e => e.SequenceId).ValueGeneratedOnAdd(); + }); + + this.OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/ConnectionStringProvider.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/ConnectionStringProvider.cs new file mode 100644 index 00000000..e79dc73a --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/ConnectionStringProvider.cs @@ -0,0 +1,30 @@ +namespace Lab.Sharding.DB; + +public interface IConnectionStringProvider +{ + string GetConnectionString(string databaseIdentifier); + + void SetConnectionStrings(Dictionary connectionStrings); +} + +public class ConnectionStringProvider : IConnectionStringProvider +{ + private Dictionary _connectionStrings; + + public ConnectionStringProvider() + { + + } + + public void SetConnectionStrings(Dictionary connectionStrings) + { + this._connectionStrings = connectionStrings; + } + + public string GetConnectionString(string databaseIdentifier) + { + return this._connectionStrings.TryGetValue(databaseIdentifier, out var connectionString) + ? connectionString + : throw new ArgumentException("Unknown database identifier"); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/IDynamicDbContextFactory.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/IDynamicDbContextFactory.cs new file mode 100644 index 00000000..57b8dcc0 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/IDynamicDbContextFactory.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; + +namespace Lab.Sharding.DB; + +public interface IDynamicDbContextFactory + where TContext : DbContext +{ + TContext CreateDbContext(string connectionString); +} + +public class DynamicDbContextFactory : IDynamicDbContextFactory + where TContext : DbContext +{ + private readonly DbContextOptionsBuilder _optionsBuilder; + private readonly IConnectionStringProvider _connectionStringProvider; + + public DynamicDbContextFactory(IConnectionStringProvider connectionStringProvider) + { + this._connectionStringProvider = connectionStringProvider; + this._optionsBuilder = new DbContextOptionsBuilder(); + } + + public TContext CreateDbContext(string databaseIdentifier) + { + var connectionString = this._connectionStringProvider.GetConnectionString(databaseIdentifier); + this._optionsBuilder.UseSqlServer(connectionString); + return (TContext)Activator.CreateInstance(typeof(TContext), _optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/Lab.Sharding.DB.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/Lab.Sharding.DB.csproj new file mode 100644 index 00000000..4a0e2409 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.DB/Lab.Sharding.DB.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeExtensions.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeExtensions.cs new file mode 100644 index 00000000..aadd6a01 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeExtensions.cs @@ -0,0 +1,38 @@ +using System.Globalization; + +namespace Lab.Sharding.Infrastructure; + +public static class DateTimeExtensions +{ + public const string DefaultDateTimeFormat = "o"; + public static readonly CultureInfo DefaultDateTimeCultureInfo = CultureInfo.InvariantCulture; + + public static IEnumerable<(DateTime start, DateTime end)> Each(this DateTime inputStart, DateTime inputEnd, + TimeSpan step) + { + DateTime dtStart, dtEnd; + dtStart = inputStart; + while (dtStart < inputEnd) + { + dtEnd = dtStart + step; + if (dtEnd > inputEnd) + { + dtEnd = inputEnd; + } + + yield return (dtStart, dtEnd); + + dtStart += step; + } + } + + public static string ToUtcString(this DateTimeOffset dto) + { + return dto.UtcDateTime.ToString(DefaultDateTimeFormat, DefaultDateTimeCultureInfo); + } + + public static string ToUtcString(this DateTime dt) + { + return dt.ToString(DefaultDateTimeFormat, DefaultDateTimeCultureInfo); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeOffsetJsonConverter.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeOffsetJsonConverter.cs new file mode 100644 index 00000000..57f19363 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/DateTimeOffsetJsonConverter.cs @@ -0,0 +1,27 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lab.Sharding.Infrastructure; + +public class DateTimeOffsetJsonConverter : JsonConverter +{ + public override DateTimeOffset Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + return DateTimeOffset.Parse( + reader.GetString(), + DateTimeExtensions.DefaultDateTimeCultureInfo, + DateTimeStyles.AdjustToUniversal); + } + + public override void Write( + Utf8JsonWriter writer, + DateTimeOffset dateTimeValue, + JsonSerializerOptions options) + { + writer.WriteStringValue(dateTimeValue.ToUtcString()); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentUtility.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentUtility.cs new file mode 100644 index 00000000..c51312f3 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentUtility.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Lab.Sharding.Infrastructure +{ + public static class EnvironmentUtility + { + public static string FindParentFolder(string parentFolderName) + { + var currentDirectory = AppDomain.CurrentDomain.BaseDirectory; + var directory = new DirectoryInfo(currentDirectory); + + while (directory != null) + { + var folders = Directory.GetDirectories(directory.FullName, parentFolderName); + if (folders.Length > 0) + { + return folders[0]; + } + + directory = directory.Parent; + } + + throw new DirectoryNotFoundException($"Folder '{parentFolderName}' not found."); + } + + public static void ReadEnvironmentFile(string folder, string fileName) + { + var fileFullPath = Path.Combine(folder, fileName); + if (!File.Exists(fileFullPath)) + { + throw new FileNotFoundException($"File '{fileName}' not found."); + } + + //讀取 .env 檔案 + var lines = File.ReadAllLines(fileFullPath); + foreach (var line in lines) + { + //排除註解 + if (line.StartsWith("#")) + { + continue; + } + + var parts = line.Split('='); + if (parts.Length < 2) + { + continue; + } + + var key = parts[0]; + var value = ""; + for (int i = 1; i < parts.Length; i++) + { + if (i == 1) + { + value += parts[i]; + continue; + } + + value += $"={parts[i]}"; + } + + Environment.SetEnvironmentVariable(key, value); + } + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentVariableBase.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentVariableBase.cs new file mode 100644 index 00000000..05028b30 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/EnvironmentVariableBase.cs @@ -0,0 +1,37 @@ +namespace Lab.Sharding.Infrastructure; + +public record EnvironmentVariableBase +{ + public string KeyName { get; } + + public T Value { get; } + + public EnvironmentVariableBase(Func converter, bool isRequired = true) + { + this.KeyName = this.GetType().Name; + + string rawValue; + try + { + rawValue = Environment.GetEnvironmentVariable(this.KeyName); + } + catch + { + rawValue = null; + } + + if (isRequired && string.IsNullOrWhiteSpace(rawValue)) + { + throw new ArgumentNullException($"EnvironmentVariable({this.KeyName}) was required."); + } + + this.Value = converter(rawValue); + } +} + +public record EnvironmentVariableBase : EnvironmentVariableBase +{ + public EnvironmentVariableBase(bool isRequired = true) : base(x => x, isRequired) + { + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/IUuidProvider.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/IUuidProvider.cs new file mode 100644 index 00000000..3ba3562c --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/IUuidProvider.cs @@ -0,0 +1,11 @@ +namespace Lab.Sharding.Infrastructure; + +public interface IUuidProvider +{ + public string NewId(); +} + +public class UuidProvider : IUuidProvider +{ + public string NewId() => Guid.NewGuid().ToString(); +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/JsonSerializeFactory.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/JsonSerializeFactory.cs new file mode 100644 index 00000000..39cf2acd --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/JsonSerializeFactory.cs @@ -0,0 +1,32 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lab.Sharding.Infrastructure; + +public class JsonSerializeFactory +{ + private static readonly Lazy s_defaultOptionLazy = new(CreateDefaultOptions); + + public static JsonSerializerOptions DefaultOptions => s_defaultOptionLazy.Value; + + public static JsonSerializerOptions CreateDefaultOptions() + { + var options = new JsonSerializerOptions(); + Apply(options); + return options; + } + + public static JsonSerializerOptions Apply(JsonSerializerOptions options) + { + options.MaxDepth = 20; + options.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + options.PropertyNameCaseInsensitive = true; + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new DateTimeOffsetJsonConverter()); + return options; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/Lab.Sharding.Infrastructure.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/Lab.Sharding.Infrastructure.csproj new file mode 100644 index 00000000..b6e3fde8 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/Lab.Sharding.Infrastructure.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextAccessor.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextAccessor.cs new file mode 100644 index 00000000..2215f6d3 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextAccessor.cs @@ -0,0 +1,23 @@ +namespace Lab.Sharding.Infrastructure.TraceContext; + +public class ContextAccessor : IContextSetter, IContextGetter + where T : class +{ + private static readonly AsyncLocal> s_current = new(); + + public T? Get() + { + var contextHolder = s_current.Value; + return contextHolder?.Value; + } + + public void Set(T value) + { + if (s_current.Value == null) + { + s_current.Value = new ContextHolder(); + } + + s_current.Value.Value = value; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextHolder.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextHolder.cs new file mode 100644 index 00000000..2124f5bb --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/ContextHolder.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.Infrastructure.TraceContext; + +public class ContextHolder +{ + public T Value { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextGetter.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextGetter.cs new file mode 100644 index 00000000..bbe85ded --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextGetter.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.Infrastructure.TraceContext; + +public interface IContextGetter +{ + T? Get(); +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextSetter.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextSetter.cs new file mode 100644 index 00000000..e8b419a7 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Infrastructure/TraceContext/IContextSetter.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.Infrastructure.TraceContext; + +public interface IContextSetter +{ + void Set(T value); +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/BaseStep.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/BaseStep.cs new file mode 100644 index 00000000..238974b8 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/BaseStep.cs @@ -0,0 +1,354 @@ +using System.Net.Mime; +using System.Text; +using System.Text.Json.JsonDiffPatch; +using System.Text.Json.JsonDiffPatch.Xunit; +using System.Text.Json.Nodes; +using FluentAssertions; +using Flurl; +using Lab.Sharding.DB; +using Lab.Sharding.Testing.Common; +using Lab.Sharding.Testing.Common.MockServer; +using Json.Path; +using Lab.Sharding.DB.AutoGenerated; +using Lab.Sharding.DB.AutoGenerated.Entities; +using Lab.Sharding.WebAPI; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Reqnroll; +using Xunit; +using Xunit.Abstractions; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +[Binding] +[CollectionDefinition("Lab.Sharding.IntegrationTest", DisableParallelization = true)] +public class BaseStep : Steps +{ + private readonly ITestOutputHelper _testOutputHelper; + private static HttpClient ExternalClient; + static IServiceProvider ServiceProvider; + + private const string StringEquals = "字串等於"; + private const string NumberEquals = "數值等於"; + private const string BoolEquals = "布林值等於"; + private const string JsonEquals = "Json等於"; + private const string DateTimeEquals = "時間等於"; + + private const string OperationTypes = StringEquals + + "|" + NumberEquals + + "|" + BoolEquals + + "|" + JsonEquals + + "|" + DateTimeEquals; + + public BaseStep(ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = testOutputHelper; + } + + [BeforeTestRun] + public static async Task BeforeTestRun() + { + //建立容器 + await CreateContainersAsync(); + TestAssistant.SetEnvironmentVariables(); + + //建立當前測試步驟所需要的 DI Containers + ServiceProvider = CreateServiceProvider(); + + await InitialDatabase(ServiceProvider); + + async Task InitialDatabase(IServiceProvider serviceProvider) + { + var dbContextFactory = serviceProvider.GetService>(); + await using var dbContext = await dbContextFactory.CreateDbContextAsync(); + await dbContext.Initial(); + } + + async Task CreateContainersAsync() + { + var msSqlContainer = await TestContainerFactory.CreateMsSqlContainerAsync(); + var dbConnectionString = msSqlContainer.GetConnectionString(); + TestAssistant.SetDbConnectionEnvironmentVariable(dbConnectionString); + var redisContainer = await TestContainerFactory.CreateRedisContainerAsync(); + var redisDomainUrl = redisContainer.GetConnectionString(); + TestAssistant.SetRedisConnectionEnvironmentVariable(redisDomainUrl); + + var mockServerContainer = await TestContainerFactory.CreateMockServerContainerAsync(); + var externalUrl = TestContainerFactory.GetMockServerConnection(mockServerContainer); + TestAssistant.SetExternalConnectionEnvironmentVariable(externalUrl); + ExternalClient = new HttpClient() { BaseAddress = new Uri(externalUrl) }; + } + } + + [BeforeScenario] + public async Task BeforeScenario() + { + this.ClearData(ServiceProvider); + } + + private static IServiceProvider CreateServiceProvider() + { + var services = new ServiceCollection(); + services.AddSysEnvironments(); + services.AddLogging(builder => builder.AddConsole()); + services.AddDatabase(); + + var serviceProvider = services.BuildServiceProvider(); + return serviceProvider; + } + + private void ClearData(IServiceProvider serviceProvider) + { + var contextFactory = serviceProvider.GetRequiredService>(); + using var dbContext = contextFactory.CreateDbContext(); + dbContext.ClearAllData(); + } + + [Given(@"資料庫已存在 Member 資料")] + public async Task Given資料庫已存在Member資料(Table table) + { + var userId = this.ScenarioContext.GetUserId(); + var now = this.ScenarioContext.GetUtcNow().Value; + var toDb = table.CreateSet(p => new Member + { + Id = null, + Name = null, + Age = null, + SequenceId = 0, + CreatedAt = now, + CreatedBy = userId, + ChangedAt = now, + ChangedBy = userId, + Email = null + }); + await using var dbContext = await this.ScenarioContext.GetMemberDbContextFactory().CreateDbContextAsync(); + await dbContext.Members.AddRangeAsync(toDb); + await dbContext.SaveChangesAsync(); + } + + [Given(@"建立假端點,HttpMethod = ""(.*)"",URL = ""(.*)"",StatusCode = ""(.*)"",ResponseContent =")] + public async Task Given建立假端點HttpMethodUrlStatusCodeResponseContent( + string httpMethod, string url, int statusCode, string body) + { + var client = ExternalClient; + await MockedServerAssistant.PutNewEndPointAsync(client, httpMethod, url, statusCode, body); + } + + [When(@"調用端發送 ""(.*)"" 請求至 ""(.*)""")] + public async Task When調用端發送請求至(string methodName, string url) + { + var client = this.ScenarioContext.GetHttpClient(); + + var httpMethod = new HttpMethod(methodName); + var urlSegments = Url.ParsePathSegments(url); + var urlEncoded = Url.Combine(urlSegments.ToArray()); + urlEncoded = this.AppendQuery(urlEncoded); + using var httpRequest = new HttpRequestMessage(httpMethod, urlEncoded); + + var contentType = MediaTypeNames.Application.Json; + var headers = this.ScenarioContext.GetOrNewHeaders(); + foreach (var header in headers) + { + if (header.Key == "content-type") + { + contentType = header.Value.First(); + } + else + { + httpRequest.Headers.Add(header.Key, header.Value.ToArray()); + } + } + + var body = this.ScenarioContext.GetHttpRequestBody(); + if (string.IsNullOrWhiteSpace(body) is false) + { + httpRequest.Content = new StringContent(body, Encoding.UTF8, contentType); + } + + var httpResponse = await client.SendAsync(httpRequest); + var responseBody = await httpResponse.Content.ReadAsStringAsync(); + this.ScenarioContext.SetHttpResponse(httpResponse); + this.ScenarioContext.SetHttpResponseBody(responseBody); + this.ScenarioContext.SetHttpStatusCode(httpResponse.StatusCode); + if (string.IsNullOrWhiteSpace(responseBody) == false) + { + Console.WriteLine(responseBody); + var jsonNode = JsonNode.Parse(responseBody); + this.ScenarioContext.SetJsonNode(jsonNode); + var jsonObject = jsonNode.AsObject(); + + if (jsonObject.TryGetPropertyValue("nextPageToken", out var nextPageTokenNode)) + { + var nextPageToken = nextPageTokenNode.GetValue(); + this.ScenarioContext.SetNextPageToken(nextPageToken); + } + } + } + + private static void ContentShouldBe(JsonNode srcJsonNode, string selectPath, string operationType, string expected) + { + var destJsonNode = JsonPath.Parse(selectPath); + switch (operationType) + { + case StringEquals: + { + var actual = destJsonNode.Evaluate(srcJsonNode).Matches.FirstOrDefault()?.Value?.GetValue(); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]"; + (actual ?? string.Empty).Should().Be(expected, errorReason); + break; + } + case NumberEquals: + { + var actual = destJsonNode.Evaluate(srcJsonNode).Matches.FirstOrDefault()?.Value?.GetValue(); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]"; + actual.Should().Be(int.Parse(expected), errorReason); + break; + } + case BoolEquals: + { + var actual = destJsonNode.Evaluate(srcJsonNode).Matches.FirstOrDefault()?.Value?.GetValue(); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]"; + actual.Should().Be(bool.Parse(expected), errorReason); + break; + } + case DateTimeEquals: + { + var expect = DateTimeOffset.Parse(expected); + var actual = destJsonNode.Evaluate(srcJsonNode).Matches.FirstOrDefault() + ?.Value + ?.GetValue() + ; + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expect}], {nameof(actual)}: [{actual}]"; + actual.Should().Be(expect, errorReason); + break; + } + case JsonEquals: + { + var actual = destJsonNode.Evaluate(srcJsonNode).Matches.FirstOrDefault()?.Value; + var expect = string.IsNullOrWhiteSpace(expected) ? null : JsonNode.Parse(expected); + var diff = actual.Diff(expect); + var errorReason = + $"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual?.ToJsonString()}], diff: [{diff?.ToJsonString()}]"; + actual.DeepEquals(expect).Should().BeTrue(errorReason); + break; + } + } + } + + private string AppendQuery(string url) + { + var flUrl = new Url(url); + foreach (var query in this.ScenarioContext.GetAllQueryString()) + { + flUrl.QueryParams.Add(query.Key, query.Value.Trim()); + + // 不能用 SetQueryParam,因為有多個相同的 querystring 如: filters,會後蓋前 + //url = url.SetQueryParam(query.Key, query.Value.Trim()); + } + + return flUrl.ToString(); + } + + [Then(@"預期回傳內容中路徑 ""(.*)"" 的""(.*)"" ""(.*)""")] + public void Then預期回傳內容中路徑的(string selectPath, string operationType, string expected) + { + var srcJsonNode = this.ScenarioContext.GetJsonNode(); + ContentShouldBe(srcJsonNode, selectPath, operationType, expected); + } + + [Then(@"預期回傳內容為")] + public void Then預期回傳內容為(string expected) + { + var actual = this.ScenarioContext.GetHttpResponseBody(); + JsonNode expectedJsonNode = JsonNode.Parse(actual); + JsonAssert.Equal(expected, actual, true); + } + + [Given(@"調用端已準備 Header 參數")] + public void Given調用端已準備Header參數(Table table) + { + foreach (var row in table.Rows) + { + foreach (var header in table.Header) + { + var value = row[header]; + if (value == "{{next-page-token}}") + { + value = this.ScenarioContext.GetNextPageToken(); + } + + this.ScenarioContext.AddHttpHeader(header, value); + } + } + } + + [Given(@"調用端已準備 Query 參數")] + public void Given調用端已準備Query參數(Table table) + { + foreach (var row in table.Rows) + { + foreach (var header in table.Header) + { + var value = row[header]; + this.ScenarioContext.AddQueryString(header, value); + } + } + } + + [Given(@"初始化測試伺服器")] + public void Given初始化測試伺服器(Table table) + { + var row = table.Rows.FirstOrDefault(); + + DateTimeOffset? now = null; + if (row.TryGetValue("Now", out var nowText)) + { + now = TestAssistant.ToUtc(nowText); + this.ScenarioContext.SetUtcNow(now); + } + + if (row.TryGetValue("UserId", out var userId)) + { + this.ScenarioContext.SetUserId(userId); + } + + var server = new TestServer(now.Value, userId); + var httpClient = server.CreateClient(); + this.ScenarioContext.SetHttpClient(httpClient); + this.ScenarioContext.SetServiceProvider(server.Services); + } + + [Given(@"調用端已準備 Body 參數\(Json\)")] + public void Given調用端已準備Body參數Json(string json) + { + this.ScenarioContext.SetHttpRequestBody(json); + } + + [Then(@"預期資料庫已存在 Member 資料為")] + public async Task Then預期資料庫已存在Member資料為(Table table) + { + await using var dbContext = await this.ScenarioContext.GetMemberDbContextFactory().CreateDbContextAsync(); + var actual = await dbContext.Members.AsNoTracking().ToListAsync(); + table.CompareToSet(actual); + } + + [Then(@"預期得到 HttpStatusCode 為 ""(.*)""")] + public void Then預期得到HttpStatusCode為(int expected) + { + var actual = (int)this.ScenarioContext.GetHttpStatusCode(); + actual.Should().Be(expected); + } + + [Then(@"預期得到 Header 為")] + public void Then預期得到Header為(Table table) + { + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DB/Scripts/NewFile1.txt b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DB/Scripts/NewFile1.txt new file mode 100644 index 00000000..e69de29b diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DbContextExtensions.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DbContextExtensions.cs new file mode 100644 index 00000000..ed14703e --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/DbContextExtensions.cs @@ -0,0 +1,60 @@ +using System.Data; +using Lab.Sharding.DB; +using Lab.Sharding.DB.AutoGenerated; +using Lab.Sharding.Testing.Common; +using Microsoft.EntityFrameworkCore; + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +public static class DbContextExtensions +{ + public static void ClearAllData(this MemberDbContext dbContext) + { + SqlServerGenerateScript.OnlySupportLocal(dbContext.Database.GetConnectionString()); + dbContext.Database.ExecuteSqlRaw(SqlServerGenerateScript.ClearAllRecord()); + } + + public static async Task Initial(this MemberDbContext dbContext) + { + SqlServerGenerateScript.OnlySupportLocal(dbContext.Database.GetConnectionString()); + // dbContext.Database.EnsureDeleted(); + + var migrations = dbContext.Database.GetMigrations(); + if (migrations != null && migrations.Any()) + { + dbContext.Database.Migrate(); + } + else + { + dbContext.Database.EnsureCreated(); + } + + await dbContext.Seed(); + } + + public static async Task Seed(this MemberDbContext dbContext) + { + SqlServerGenerateScript.OnlySupportLocal(dbContext.Database.GetConnectionString()); + + var dbConnection = dbContext.Database.GetDbConnection(); + if (dbConnection.State != ConnectionState.Open) + { + await dbConnection.OpenAsync(); + } + + //讀取資料夾的所有 sql 檔案,並執行 + var sqlFiles = Directory.GetFiles("DB/Scripts", "*.sql"); + + foreach (var sqlFile in sqlFiles) + { + var sql = await File.ReadAllTextAsync(sqlFile); + await using var cmd = dbConnection.CreateCommand(); + + // Iterate the string array and execute each one. + cmd.CommandText = sql; + await cmd.ExecuteNonQueryAsync(); + + // dbContext.Database.ExecuteSqlRaw(sql); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Lab.Sharding.IntegrationTest.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Lab.Sharding.IntegrationTest.csproj new file mode 100644 index 00000000..0c9e94a8 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Lab.Sharding.IntegrationTest.csproj @@ -0,0 +1,50 @@ + + + + net8.0 + enable + enable + + false + + JobBank1111.Job.WebAPI.IntegrationTest + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + Always + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ScenarioContextExtension.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ScenarioContextExtension.cs new file mode 100644 index 00000000..9c97b87b --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ScenarioContextExtension.cs @@ -0,0 +1,144 @@ +using System.Net; +using System.Text; +using System.Text.Json.Nodes; +using Lab.Sharding.DB; +using Lab.Sharding.DB.AutoGenerated; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Reqnroll; + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +public static class ScenarioContextExtension +{ + public static T? GetOrDefault(this ScenarioContext context, string key, T? defaultValue = default) => + context.ContainsKey(key) ? context.Get(key) : defaultValue; + + public static void SetServiceProvider(this ScenarioContext context, IServiceProvider serviceProvider) + { + context.Set(serviceProvider); + } + + public static IServiceProvider GetServiceProvider(this ScenarioContext context) + { + return context.Get(); + } + + public static IDbContextFactory GetMemberDbContextFactory(this ScenarioContext context) + { + return GetServiceProvider(context).GetService>(); + } + + public static string? GetUserId(this ScenarioContext context) + => context.TryGetValue($"UserId", out string userId) + ? userId + : null; + + public static void SetUserId(this ScenarioContext context, string userId) => context.Set(userId, $"UserId"); + + public static DateTimeOffset? GetUtcNow(this ScenarioContext context) => + context.TryGetValue($"UtcNow", out DateTimeOffset dateTime) + ? dateTime + : null; + + public static void SetUtcNow(this ScenarioContext context, DateTimeOffset? dateTime) => + context.Set(dateTime, $"UtcNow"); + + public static long? GetFirmId(this ScenarioContext context) => + context.TryGetValue($"FirmId", out long firmId) + ? firmId + : null; + + public static void SetHttpClient(this ScenarioContext context, HttpClient httpClient) => + context.Set(httpClient); + + public static HttpClient GetHttpClient(this ScenarioContext context) => + context.Get(); + + public static void AddQueryString(this ScenarioContext context, string key, string value) + { + if (!context.TryGetValue>("QueryString", out var data)) + { + data = new List<(string Key, string Value)>(); + } + + data.Add((key, value)); + context.Set(data, "QueryString"); + } + + public static IList<(string Key, string Value)> GetAllQueryString(this ScenarioContext context) + { + return context.TryGetValue(out IList<(string Key, string Value)> result) + ? result + : new List<(string Key, string Value)>(); + } + + public static void SetHttpResponse(this ScenarioContext context, HttpResponseMessage response) => + context.Set(response); + + public static HttpResponseMessage GetHttpResponse(this ScenarioContext context) => + context.TryGetValue(out HttpResponseMessage result) ? result : default; + + public static void SetHttpResponseBody(this ScenarioContext context, string body) => + context.Set(body, "HttpResponseBody"); + + public static string GetHttpResponseBody(this ScenarioContext context) => + context.TryGetValue("HttpResponseBody", out string body) ? body : null; + + public static void SetHttpStatusCode(this ScenarioContext context, HttpStatusCode httpStatusCode) => + context.Set(httpStatusCode, "HttpStatusCode"); + + public static HttpStatusCode GetHttpStatusCode(this ScenarioContext context) => + context.Get("HttpStatusCode"); + + public static void SetNextPageToken(this ScenarioContext context, string nextPageToken) => + context.Set(nextPageToken, "NextPageToken"); + + public static string GetNextPageToken(this ScenarioContext context) => + context.Get("NextPageToken"); + + public static void SetXUnitLog(this ScenarioContext context, StringBuilder stringBuilder) + { + context.Set(stringBuilder, "XUnitLog"); + } + + public static StringBuilder GetXUnitLog(this ScenarioContext context) + { + context.TryGetValue("XUnitLog", out StringBuilder? stringBuilder); + return stringBuilder ?? new StringBuilder(); + } + + public static void AddHttpHeader(this ScenarioContext context, string key, string value) + { + var headers = context.GetOrNewHeaders(); + headers[key] = value; + context.Set(headers); + } + + public static IHeaderDictionary GetOrNewHeaders(this ScenarioContext context) + { + if (context.TryGetValue(out IHeaderDictionary result) is false) + { + result = new HeaderDictionary(); + } + + return result; + } + + public static void SetHttpRequestBody(this ScenarioContext context, string body) => + context.Set(body, "HttpRequestBody"); + + public static string? GetHttpRequestBody(this ScenarioContext context) => + context.GetOrDefault($"HttpRequestBody"); + + public static void SetJsonNode(this ScenarioContext context, JsonNode input) + { + context.Set(input); + } + + public static JsonNode GetJsonNode(this ScenarioContext context) + { + return context.Get(); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ServiceCollectionExtension.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ServiceCollectionExtension.cs new file mode 100644 index 00000000..16595558 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/ServiceCollectionExtension.cs @@ -0,0 +1,26 @@ +using Lab.Sharding.Infrastructure.TraceContext; +using Lab.Sharding.WebAPI; +using Microsoft.Extensions.DependencyInjection; + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection AddFakeContextAccessor(this IServiceCollection services, string userId) + { + services.AddSingleton>(p => + { + var traceContext = new TraceContext + { + TraceId = "測試", + UserId = userId + }; + var accessor = new ContextAccessor(); + accessor.Set(traceContext); + return accessor; + }); + services.AddSingleton>(p => p.GetService>()); + services.AddSingleton>(p => p.GetService>()); + return services; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestAssistant.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestAssistant.cs new file mode 100644 index 00000000..a03b629b --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestAssistant.cs @@ -0,0 +1,40 @@ +using Lab.Sharding.WebAPI; + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +public enum MyEnum +{ + MyProperty = 0, +} + +class TestAssistant +{ + public static void SetEnvironmentVariables() + { + Environment.SetEnvironmentVariable("JOB1111_ENVIRONMENT", "QA"); + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); + Environment.SetEnvironmentVariable(nameof(EXTERNAL_API), "http://localhost:5000/api"); + } + + public static void SetDbConnectionEnvironmentVariable(string connectionString) + { + Environment.SetEnvironmentVariable(nameof(SYS_DATABASE_CONNECTION_STRING1), connectionString); + } + + public static void SetRedisConnectionEnvironmentVariable(string url) + { + Environment.SetEnvironmentVariable(nameof(SYS_REDIS_URL), url); + } + + public static void SetExternalConnectionEnvironmentVariable(string url) + { + Environment.SetEnvironmentVariable(nameof(EXTERNAL_API), url); + } + + public static DateTime ToUtc(string time) + { + var tempTime = DateTimeOffset.Parse(time); + var utcTime = new DateTimeOffset(tempTime.DateTime, TimeSpan.Zero).UtcDateTime; + return utcTime; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestServer.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestServer.cs new file mode 100644 index 00000000..15dc8365 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/TestServer.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Time.Testing; +using Microsoft.VisualStudio.TestPlatform.TestHost; + +namespace JobBank1111.Job.WebAPI.IntegrationTest; + +public class TestServer(DateTimeOffset now, + string userId) + : WebApplicationFactory +{ + private void ConfigureServices(IServiceCollection services) + { + //模擬身分 + services.AddFakeContextAccessor(userId); + + //模擬現在時間 + var fakeTimeProvider = new FakeTimeProvider(now); + services.AddSingleton(fakeTimeProvider); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(this.ConfigureServices); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Usings.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git "a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222.feature" "b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222.feature" new file mode 100644 index 00000000..004bb0fd --- /dev/null +++ "b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222.feature" @@ -0,0 +1,176 @@ +Feature: 飯粒 + + Background: + Given 調用端已準備 Header 參數 + | x-trace-id | + | TW | + Given 調用端已準備 Query 參數 + | select-profile | + | avatarUrl | + Given 初始化測試伺服器 + | Now | UserId | + | 2000-01-01T00:00:00+00:00 | yao | + + Scenario: 新增一筆會員 + Given 調用端已準備 Body 參數(Json) + """ + { + "email": "yao@9527", + "name": "yao", + "age": 18 + } + """ + When 調用端發送 "POST" 請求至 "api/v1/members" + Then 預期得到 HttpStatusCode 為 "204" + Then 預期資料庫已存在 Member 資料為 + | Email | Name | Age | CreatedAt | CreatedAt | + | yao@9527 | yao | 18 | 2000-01-01T00:00:00+00:00 | 2000-01-01T00:00:00+00:00 | + + Scenario: 查詢所有會員 offset + Given 資料庫已存在 Member 資料 + | Id | Email | Name | Age | + | 1 | yao@9527 | yao1 | 18 | + | 2 | yao@9528 | yao2 | 18 | + | 3 | yao@9529 | yao3 | 18 | + Given 調用端已準備 Header 參數 + | x-page-size | x-page-index | cache-control | + | 2 | 0 | no-cache | + When 調用端發送 "GET" 請求至 "api/v1/members:offset" + Then 預期得到 HttpStatusCode 為 "200" + Then 預期得到 Header 為 + | page-size | page-index | row-total | + | 2 | 0 | 3 | + Then 預期回傳內容為 + """ + { + "items": [ + { + "id": "1", + "name": "yao1", + "age": 18, + "email": "yao@9527", + "sequenceId": null + }, + { + "id": "2", + "name": "yao2", + "age": 18, + "email": "yao@9528", + "sequenceId": null + } + ], + "pageIndex": 0, + "totalPages": 2, + "hasPreviousPage": false, + "hasNextPage": true + } + """ + + Scenario: 查詢所有會員 cursor + Given 資料庫已存在 Member 資料 + | Id | Email | Name | Age | + | 1 | yao@9527 | yao1 | 18 | + | 2 | yao@9528 | yao2 | 18 | + | 3 | yao@9529 | yao3 | 18 | + Given 調用端已準備 Header 參數 + | x-page-size | x-next-page-token | + | 1 | | + When 調用端發送 "GET" 請求至 "api/v1/members:cursor" + Then 預期得到 HttpStatusCode 為 "200" + Then 預期回傳內容為 + """ + { + "items": [ + { + "id": "1", + "name": "yao1", + "age": 18, + "email": "yao@9527", + "sequenceId": 1 + } + ], + "nextPageToken": "eyJsYXN0SWQiOiIxIiwibGFzdFNlcXVlbmNlSWQiOjF9", + "nextPreviousToken": null + } + """ + Then 預期得到 HttpStatusCode 為 "200" + Given 調用端已準備 Header 參數 + | x-page-size | x-next-page-token | + | 1 | {{next-page-token}} | + When 調用端發送 "GET" 請求至 "api/v1/members:cursor" + Then 預期得到 HttpStatusCode 為 "200" + Then 預期回傳內容為 + """ + { + "items": [ + { + "id": "2", + "name": "yao2", + "age": 18, + "email": "yao@9528", + "sequenceId": 2 + } + ], + "nextPageToken": "eyJsYXN0SWQiOiIyIiwibGFzdFNlcXVlbmNlSWQiOjJ9", + "nextPreviousToken": null + } + """ + + Scenario: 外部服務 + Given 資料庫已存在 Member 資料 + | Id | Email | Name | Age | + | 1 | yao@9527 | yao1 | 18 | + Given 建立假端點,HttpMethod = "POST",URL = "/ec/V1/SalePage/UpdateStock",StatusCode = "200",ResponseContent = + """ + { + "ErrorId": "", + "Status": "Success", + "Data": "", + "ErrorMessage": null, + "TimeStamp": "2024-02-21T16:55:21.4988154+08:00" + } + """ + + Scenario: 用 JsonDiff 驗證資料 + When 模擬呼叫 API,得到以下內容 + """ + { + "Id": 1, + "Age": 18, + "Birthday": "2000-01-01T00:00:00+00:00", + "FullName": { + "FirstName": "John", + "LastName": "Doe" + } + } + """ + Then 預期回傳內容為 + """ + { + "Id": 1, + "Age": 18, + "Birthday": "2000-01-01T00:00:00+00:00", + "FullName": { + "FirstName": "John", + "LastName": "Doe" + } + } + """ + + Scenario: 用 JsonPath 驗證資料 + Given 已存在 Json 內容 + """ + { + "Id": 1, + "Age": 18, + "Birthday": "2000-01-01T00:00:00+00:00", + "FullName": { + "FirstName": "John", + "LastName": "Doe" + } + } + """ + Then 預期回傳內容中路徑 "$.Age" 的"數值等於" "18" + Then 預期回傳內容中路徑 "$.Birthday" 的"時間等於" "2000-01-01T00:00:00+00:00" + Then 預期回傳內容中路徑 "$.FullName.FirstName" 的"字串等於" "John" + Then 預期回傳內容中路徑 "$.FullName.LastName" 的"字串等於" "Doe" \ No newline at end of file diff --git "a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222Step.cs" "b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222Step.cs" new file mode 100644 index 00000000..eec52713 --- /dev/null +++ "b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.IntegrationTest/_01_Demo/\351\243\257\347\262\222Step.cs" @@ -0,0 +1,21 @@ +using System.Text.Json.Nodes; +using Reqnroll; + +namespace JobBank1111.Job.WebAPI.IntegrationTest._01_Demo; + +[Binding] +public class 飯粒Step : Steps +{ + [Given(@"已存在 Json 內容")] + public void Given已存在Json內容(string json) + { + var jsonNode = JsonNode.Parse(json); + this.ScenarioContext.SetJsonNode(jsonNode); + } + + [When(@"模擬呼叫 API,得到以下內容")] + public void When模擬呼叫api得到以下內容(string json) + { + this.ScenarioContext.SetHttpResponseBody(json); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/Lab.Sharding.Test.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/Lab.Sharding.Test.csproj new file mode 100644 index 00000000..6831b96d --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/Lab.Sharding.Test.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/UnitTest1.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/UnitTest1.cs new file mode 100644 index 00000000..e3d2be2c --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Test/UnitTest1.cs @@ -0,0 +1,19 @@ +using Lab.Sharding.WebAPI; +using Lab.Sharding.WebAPI.Contract; +using Newtonsoft.Json; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace Lab.Sharding.Test; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + var data = new Lab.Sharding.WebAPI.PaginatedList(); + + var json = "{\"items\":[{\"id\":\"1a6bd961-7a8b-470d-9282-2420c1daa211\",\"name\":\"yao\",\"age\":20,\"email\":\"yao@9527\"},{\"id\":\"93737ff9-a162-4d2e-92ac-b1944f5cce90\",\"name\":\"yao2\",\"age\":18,\"email\":\"9527@yao\"}],\"pageIndex\":0,\"totalPages\":1,\"hasPreviousPage\":false,\"hasNextPage\":true}"; + var paginatedList = JsonSerializer.Deserialize>(json); + var deserializeObject = JsonConvert.DeserializeObject>(json); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/Lab.Sharding.Testing.Common.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/Lab.Sharding.Testing.Common.csproj new file mode 100644 index 00000000..511def9f --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/Lab.Sharding.Testing.Common.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/Cookies.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/Cookies.cs new file mode 100644 index 00000000..f138ef3b --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/Cookies.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.Testing.Common.MockServer.Contracts; + +public class Cookies +{ + public string Session { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpRequest.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpRequest.cs new file mode 100644 index 00000000..78b327b5 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpRequest.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Lab.Sharding.Testing.Common.MockServer.Contracts; + +public class HttpRequest +{ + public string Method { get; set; } + public string Path { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Cookies Cookies { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpResponse.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpResponse.cs new file mode 100644 index 00000000..846e40d5 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/HttpResponse.cs @@ -0,0 +1,8 @@ +namespace Lab.Sharding.Testing.Common.MockServer.Contracts; + +public class HttpResponse +{ + public string Body { get; set; } + + public int StatusCode { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/PutNewEndPointRequest.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/PutNewEndPointRequest.cs new file mode 100644 index 00000000..d8127ba4 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/Contracts/PutNewEndPointRequest.cs @@ -0,0 +1,7 @@ +namespace Lab.Sharding.Testing.Common.MockServer.Contracts; + +public class PutNewEndPointRequest +{ + public HttpRequest HttpRequest { get; set; } + public HttpResponse HttpResponse { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockServerHelper.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockServerHelper.cs new file mode 100644 index 00000000..482a4d34 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockServerHelper.cs @@ -0,0 +1,68 @@ +// using System.Text; +// using System.Text.Json; +// using DotNet.Testcontainers.Builders; +// using DotNet.Testcontainers.Containers; +// using Lab.Sharding.Testing.Common.MockServers.Contracts; +// +// namespace Lab.Sharding.Testing.Common.MockServers; +// +// public static class MockServerProvider +// { +// private const int internalPort = 1080; +// public static int Port => 55124; +// private static string Image => "mockserver/mockserver"; +// public static string Hostname => $"http://{_container.Value.Hostname}:{Port}"; +// +// public static IContainer CreateContainer() => _container.Value; +// +// private static Lazy _container = new(() => new ContainerBuilder().WithImage(Image).WithPortBinding(Port,internalPort).Build()); +// private static Lazy _httpClient = new(() => new HttpClient +// { +// BaseAddress = new Uri(Hostname) +// }); +// +// public static async Task PutNewEndPoint(string httpMethod, string relativePath, int statusCode, string source) +// { +// +// var client = _httpClient.Value!; +// +// var requestModel = new PutNewEndPointRequest +// { +// HttpRequest = new HttpRequest +// { +// Method = httpMethod.ToUpper(), +// Path = relativePath.StartsWith("/") ? relativePath : $"/{relativePath}", +// }, +// HttpResponse =new HttpResponse +// { +// Body = source, +// StatusCode = statusCode +// } +// }; +// +// var content = JsonSerializer.Serialize( +// requestModel, +// new JsonSerializerOptions +// { +// +// PropertyNamingPolicy = JsonNamingPolicy.CamelCase, +// }); +// +// using var request = new HttpRequestMessage(HttpMethod.Put, $"{Hostname}/mockserver/expectation") +// { +// Content = new StringContent(content, Encoding.UTF8, "application/json") +// }; +// +// using var response = await client.SendAsync(request); +// +// response.EnsureSuccessStatusCode(); +// } +// +// public static async Task ResetAsync() +// { +// var client = _httpClient.Value!; +// using var request = new HttpRequestMessage(HttpMethod.Put, $"{Hostname}/mockserver/reset"); +// using var response = await client.SendAsync(request); +// response.EnsureSuccessStatusCode(); +// } +// } \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockedServerAssistant.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockedServerAssistant.cs new file mode 100644 index 00000000..76ca7aa4 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/MockServer/MockedServerAssistant.cs @@ -0,0 +1,45 @@ +using System.Text; +using System.Text.Json; +using Lab.Sharding.Testing.Common.MockServer.Contracts; + +namespace Lab.Sharding.Testing.Common.MockServer; + +public class MockedServerAssistant +{ + public static async Task ResetAsync(HttpClient client) + { + using var request = new HttpRequestMessage(HttpMethod.Put, $"mockserver/reset"); + using var response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + public static async Task PutNewEndPointAsync(HttpClient client, + string httpMethod, + string relativePath, + int statusCode, + string body) + { + var requestModel = new PutNewEndPointRequest + { + HttpRequest = new HttpRequest + { + Method = httpMethod.ToUpper(), + Path = relativePath.StartsWith("/") ? relativePath : $"/{relativePath}", + }, + HttpResponse = new HttpResponse { Body = body, StatusCode = statusCode } + }; + + var content = JsonSerializer.Serialize( + requestModel, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); + + using var request = new HttpRequestMessage(HttpMethod.Put, $"mockserver/expectation") + { + Content = new StringContent(content, Encoding.UTF8, "application/json") + }; + + using var response = await client.SendAsync(request); + + response.EnsureSuccessStatusCode(); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/NpgsqlGenerateScript.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/NpgsqlGenerateScript.cs new file mode 100644 index 00000000..2eb87480 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/NpgsqlGenerateScript.cs @@ -0,0 +1,58 @@ +namespace Lab.Sharding.Testing.Common; + +public class NpgsqlGenerateScript +{ + public static string ClearAllRecord() + { + return @" +DO $$ +DECLARE row RECORD; +DECLARE seq TEXT; +BEGIN + FOR row IN SELECT table_name + FROM information_schema.tables + WHERE table_type='BASE TABLE' + AND table_schema='public' + AND table_name NOT IN ('admins', 'admin_roles', '__EFMigrationsHistory') + LOOP + EXECUTE format('TRUNCATE TABLE %I CONTINUE IDENTITY CASCADE;', row.table_name); + END LOOP; + FOR row IN SELECT table_name + FROM information_schema.tables + WHERE table_type='BASE TABLE' + AND table_schema='histories' + AND table_name NOT IN ('admins', 'admin_roles', '__EFMigrationsHistory') + LOOP + EXECUTE format('TRUNCATE TABLE histories.%I CONTINUE IDENTITY CASCADE;', row.table_name); + END LOOP; + FOR seq IN (select sequencename FROM pg_sequences) LOOP + EXECUTE 'ALTER SEQUENCE IF EXISTS '||seq||' RESTART WITH 1;'; + END LOOP; +END; +$$; +"; + } + + public static string CreateChannelIdSeq() + { + return "Create SEQUENCE channel_channel_id_seq RESTART WITH 1;"; + } + + public static string ReseedChannelIdSeq() + { + return "ALTER SEQUENCE channel_channel_id_seq RESTART WITH 1;"; + } + + // public static void OnlySupportLocal(string connectionString) + // { + // var builder = new NpgsqlConnectionStringBuilder(connectionString); + // if (string.Compare(builder.Host, "LOCALHOST", StringComparison.InvariantCultureIgnoreCase) != 0 + // && string.Compare(builder.Host, "127.0.0.1", StringComparison.InvariantCultureIgnoreCase) != 0 + // && string.Compare(builder.Host, "172.17.0.1", StringComparison.InvariantCultureIgnoreCase) != + // 0 // docker 建立容器時的預設位置 + // ) + // { + // throw new NotSupportedException($"伺服器只支援 localhost,目前連線字串為 {connectionString}"); + // } + // } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/RedirectConsole.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/RedirectConsole.cs new file mode 100644 index 00000000..353842ac --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/RedirectConsole.cs @@ -0,0 +1,22 @@ +namespace Lab.Sharding.Testing.Common; + +public sealed class RedirectConsole : IDisposable +{ + private readonly Action _logFunction; + private readonly TextWriter _oldOut = Console.Out; + private readonly StringWriter sw = new StringWriter(); + + public RedirectConsole(Action logFunction) + { + this._logFunction = logFunction; + Console.SetOut(this.sw); + } + + public void Dispose() + { + Console.SetOut(this._oldOut); + this.sw.Flush(); + this._logFunction(this.sw.ToString()); + this.sw.Dispose(); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/SqlServerGenerateScript.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/SqlServerGenerateScript.cs new file mode 100644 index 00000000..5d8d380e --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/SqlServerGenerateScript.cs @@ -0,0 +1,32 @@ +using Microsoft.Data.SqlClient; + +namespace Lab.Sharding.Testing.Common; + +public class SqlServerGenerateScript +{ + public static string ClearAllRecord() + { + return @" +EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL' +EXEC sp_MSForEachTable 'SET QUOTED_IDENTIFIER ON; DELETE FROM ?' +EXEC sp_MSForEachTable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL' +"; + } + + public static void OnlySupportLocal(string connectionString) + { + var allowData = new List() + { + "localhost", + "127.0.0.1", + "172.17.0.1" //localhost in docker + }; + var builder = new SqlConnectionStringBuilder(connectionString); + var dataSource = builder.DataSource.Split(',')[0]; // Extract the IP part + var contains = allowData.Contains(dataSource); + if (contains == false) + { + throw new NotSupportedException($"伺服器只支援 localhost,目前連線字串為 {connectionString}"); + } + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/TestContainerFactory.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/TestContainerFactory.cs new file mode 100644 index 00000000..549b17f5 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.Testing.Common/TestContainerFactory.cs @@ -0,0 +1,66 @@ +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using Testcontainers.MsSql; +using Testcontainers.PostgreSql; +using Testcontainers.Redis; + +namespace Lab.Sharding.Testing.Common; + +public class TestContainerFactory +{ + // TODO:docker hub 有訪問次數限制,需要一台 proxy server + public static async Task CreateRedisContainerAsync() + { + var redisContainer = new RedisBuilder() + .WithImage("redis:7.0") + .Build(); + await redisContainer.StartAsync(); + return redisContainer; + } + + public static async Task CreateMsSqlContainerAsync() + { + var container = new MsSqlBuilder() + .WithName("sql2019") + .WithImage("mcr.microsoft.com/mssql/server:2019-latest") + .WithPassword("pass@w0rd1~") + .WithEnvironment("ACCEPT_EULA", "Y") + .WithEnvironment("MSSQL_PID", "Developer") + .WithPortBinding(1433, assignRandomHostPort: true) + .Build(); + await container.StartAsync(); + return container; + } + + public static async Task CreatePostgreSqlContainerAsync() + { + var waitStrategy = Wait.ForUnixContainer().UntilCommandIsCompleted("pg_isready"); + var container = new PostgreSqlBuilder() + .WithImage("postgres:13-alpine") + .WithName("postgres.13") + .WithPortBinding(5432, assignRandomHostPort: true) + .WithWaitStrategy(waitStrategy) + .WithUsername("postgres") + .WithPassword("postgres") + .Build(); + await container.StartAsync(); + return container; + } + + public static async Task CreateMockServerContainerAsync() + { + var container = new ContainerBuilder() + .WithName("mockserver") + .WithImage("mockserver/mockserver") + .WithPortBinding(1080, assignRandomHostPort: true) + .Build(); + await container.StartAsync(); + return container; + } + + public static string GetMockServerConnection(IContainer container) + { + var port = container.GetMappedPublicPort(1080); + return $"http://{container.Hostname}:{port}"; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CacheKeys.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CacheKeys.cs new file mode 100644 index 00000000..d96e5a30 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CacheKeys.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.WebAPI; + +public enum CacheKeys +{ + MemberData, +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Contract/AutoGenerated/ContractControllers.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Contract/AutoGenerated/ContractControllers.cs new file mode 100644 index 00000000..43ab2c0f --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Contract/AutoGenerated/ContractControllers.cs @@ -0,0 +1,213 @@ +//---------------------- +// +// Generated using the NSwag toolchain v14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.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 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" +#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" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" +#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" +#pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." + +namespace Lab.Sharding.WebAPI.Contract +{ + using System = global::System; + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public interface ITagController + { + + /// next page token + + /// OK + + System.Threading.Tasks.Task> GetTagsCursorAsync(string x_next_page_token, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + + public partial class TagController : Microsoft.AspNetCore.Mvc.ControllerBase + { + private ITagController _implementation; + + public TagController(ITagController implementation) + { + _implementation = implementation; + } + + /// next page token + /// OK + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("api/v2/tags:cursor")] + public System.Threading.Tasks.Task> GetTagsCursor([Microsoft.AspNetCore.Mvc.FromHeader(Name = "x-next-page-token")] string x_next_page_token, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.GetTagsCursorAsync(x_next_page_token, cancellationToken); + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public interface IMemberController + { + + /// OK + + System.Threading.Tasks.Task> GetMembersCursorAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + + /// OK + + System.Threading.Tasks.Task> GetMemberOffsetAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + + /// OK + + System.Threading.Tasks.Task InsertMember1Async(InsertMemberRequest body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + + public partial class MemberController : Microsoft.AspNetCore.Mvc.ControllerBase + { + private IMemberController _implementation; + + public MemberController(IMemberController implementation) + { + _implementation = implementation; + } + + /// OK + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("api/v2/members:cursor")] + public System.Threading.Tasks.Task> GetMembersCursor(System.Threading.CancellationToken cancellationToken) + { + + return _implementation.GetMembersCursorAsync(cancellationToken); + } + + /// OK + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("api/v2/members:offset")] + public System.Threading.Tasks.Task> GetMemberOffset(System.Threading.CancellationToken cancellationToken) + { + + return _implementation.GetMemberOffsetAsync(cancellationToken); + } + + /// OK + [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("api/v2/members")] + public System.Threading.Tasks.Task InsertMember1([Microsoft.AspNetCore.Mvc.FromBody] InsertMemberRequest body, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.InsertMember1Async(body, cancellationToken); + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GetMemberResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + public string Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string Name { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("age")] + public int? Age { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("email")] + public string Email { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("sequenceId")] + public long? SequenceId { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GetMemberResponseCursorPaginatedList + { + + [System.Text.Json.Serialization.JsonPropertyName("items")] + public System.Collections.Generic.List Items { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("nextPageToken")] + public string NextPageToken { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("nextPreviousToken")] + public string NextPreviousToken { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GetMemberResponsePaginatedList + { + + [System.Text.Json.Serialization.JsonPropertyName("items")] + public System.Collections.Generic.List Items { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("pageIndex")] + public int PageIndex { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("totalPages")] + public int TotalPages { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("hasPreviousPage")] + public bool HasPreviousPage { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("hasNextPage")] + public bool HasNextPage { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class InsertMemberRequest + { + + [System.Text.Json.Serialization.JsonPropertyName("email")] + public string Email { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string Name { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("age")] + public int Age { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Failure + { + + [System.Text.Json.Serialization.JsonPropertyName("code")] + public string Code { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("reason")] + public string Reason { get; set; } + + } + + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 +#pragma warning restore 8604 +#pragma warning restore 8625 \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CursorPaginatedList.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CursorPaginatedList.cs new file mode 100644 index 00000000..f3e8693a --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/CursorPaginatedList.cs @@ -0,0 +1,17 @@ +namespace Lab.Sharding.WebAPI; + +public class CursorPaginatedList +{ + public List Items { get; } + + public string NextPageToken { get; set; } + + public string NextPreviousToken { get; set; } + + public CursorPaginatedList(List items, string nextPageToken, string nextPreviousToken) + { + this.Items = items; + this.NextPageToken = nextPageToken; + this.NextPreviousToken = nextPreviousToken; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/EnvironmentVariables.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/EnvironmentVariables.cs new file mode 100644 index 00000000..88ba33d7 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/EnvironmentVariables.cs @@ -0,0 +1,13 @@ +using Lab.Sharding.Infrastructure; + +namespace Lab.Sharding.WebAPI; + +public record ASPNETCORE_ENVIRONMENT : EnvironmentVariableBase; + +public record SYS_DATABASE_CONNECTION_STRING1 : EnvironmentVariableBase; + +public record SYS_DATABASE_CONNECTION_STRING2 : EnvironmentVariableBase; + +public record SYS_REDIS_URL : EnvironmentVariableBase; + +public record EXTERNAL_API : EnvironmentVariableBase; \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Failure.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Failure.cs new file mode 100644 index 00000000..600f265b --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Failure.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace Lab.Sharding.WebAPI; + +public class Failure +{ + public Failure() + { + } + + public Failure(string code, string message) + { + this.Code = code; + this.Message = message; + } + + /// + /// 錯誤碼 + /// + public string Code { get; init; } + + /// + /// 錯誤訊息 + /// + public string Message { get; init; } + + /// + /// 錯誤發生時的資料 + /// + public object Data { get; init; } + + /// + /// 追蹤 Id + /// + public string TraceId { get; set; } + + /// + /// 例外,不回傳給 Web API + /// + [JsonIgnore] + public Exception Exception { get; set; } + + public List Details { get; init; } = new(); +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/FailureCode.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/FailureCode.cs new file mode 100644 index 00000000..16dd06ba --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/FailureCode.cs @@ -0,0 +1,8 @@ +namespace Lab.Sharding.WebAPI; + +public enum FailureCode +{ + Unauthorized, + DbError, + DuplicateEmail +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Lab.Sharding.WebAPI.csproj b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Lab.Sharding.WebAPI.csproj new file mode 100644 index 00000000..c497dabf --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Lab.Sharding.WebAPI.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/GetMemberResponse.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/GetMemberResponse.cs new file mode 100644 index 00000000..d58b3a9e --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/GetMemberResponse.cs @@ -0,0 +1,14 @@ +namespace Lab.Sharding.WebAPI.Member; + +public class GetMemberResponse +{ + public string Id { get; set; } + + public string? Name { get; set; } + + public int? Age { get; set; } + + public string Email { get; set; } + + public long? SequenceId { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/InsertMemberRequest.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/InsertMemberRequest.cs new file mode 100644 index 00000000..b02c2e87 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/InsertMemberRequest.cs @@ -0,0 +1,10 @@ +namespace Lab.Sharding.WebAPI.Member; + +public class InsertMemberRequest +{ + public string Email { get; set; } + + public string Name { get; set; } + + public int Age { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/Member.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/Member.cs new file mode 100644 index 00000000..df8f15cd --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/Member.cs @@ -0,0 +1,20 @@ +namespace Lab.Sharding.WebAPI.Member; + +public class Member +{ + public string Id { get; set; } + + public string? Name { get; set; } + + public int? Age { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string? CreatedBy { get; set; } + + public DateTimeOffset? ChangedAt { get; set; } + + public string? ChangedBy { get; set; } + + public string Email { get; set; } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberChain.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberChain.cs new file mode 100644 index 00000000..01ac6157 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberChain.cs @@ -0,0 +1,66 @@ +using CSharpFunctionalExtensions; +using Lab.Sharding.Infrastructure.TraceContext; + +namespace Lab.Sharding.WebAPI.Member; + +public static class MemberChain +{ + + // 檢查是否有重複的 Email + public static Result + ValidateEmail(this Result previousResult, + InsertMemberRequest dest) + { + if (previousResult.IsFailure) + { + return previousResult; + } + + var src = previousResult.Value; + if (src == null) + { + return Result.Success(src); + } + + if (src.Email == dest.Email) + { + return Result.Failure(new Failure + { + Code = nameof(FailureCode.DuplicateEmail), + Message = "Email 重複", + Data = src, + }); + } + + return Result.Success(src); + } + + // 檢查是否有重複的 Email + public static Result + ValidateName(this Result previousResult, + InsertMemberRequest dest) + { + if (previousResult.IsFailure) + { + return previousResult; + } + + var src = previousResult.Value; + if (src == null) + { + return Result.Success(src); + } + + if (src.Email == dest.Email) + { + return Result.Failure(new Failure + { + Code = nameof(FailureCode.DuplicateEmail), + Message = "Email 重複", + Data = src, + }); + } + + return Result.Success(src); + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberController.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberController.cs new file mode 100644 index 00000000..e12cfc13 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberController.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.Sharding.WebAPI.Member; + +[ApiController] +public class MemberV1Controller( + MemberHandler memberHandler) : ControllerBase +{ + // [HttpGet] + // [Route("api/v1/members:cursor", Name = "GetMemberCursor")] + // public async Task>> GetMemberCursor( + // CancellationToken cancel = default) + // { + // var noCache = true; + // var pageSize = this.TryGetPageSize(); + // var nextPageToken = this.TryGetPageToken(); + // var result = await memberHandler.GetMembersAsync(pageSize, nextPageToken, noCache, cancel); + // return this.Ok(result); + // } + + [HttpGet] + [Route("api/v1/members:offset", Name = "GetMemberOffset")] + public async Task>> GetMemberOffset( + CancellationToken cancel = default) + { + var pageSize = 10; + var pageIndex = 0; + var noCache = true; + if (this.Request.Headers.TryGetValue("x-page-index", out var pageIndexText)) + { + int.TryParse(pageIndexText, out pageIndex); + } + + if (this.Request.Headers.TryGetValue("x-page-size", out var pageSizeText)) + { + int.TryParse(pageSizeText, out pageSize); + } + + if (this.Request.Headers.TryGetValue("cache-control", out var noCacheText)) + { + bool.TryParse(noCacheText, out noCache); + } + + var result = await memberHandler.GetMembersAsync(pageIndex, pageSize, noCache, cancel); + return this.Ok(result); + } + + // [HttpPost] + // [Route("api/v1/members", Name = "InsertMember1")] + // public async Task InsertMemberAsync(InsertMemberRequest request, + // CancellationToken cancel = default) + // { + // var result = await memberHandler.InsertAsync(request, cancel); + // if (result.IsFailure) + // { + // if (result.TryGetError(out var failure)) + // { + // return this.BadRequest(failure); + // } + // } + // + // return this.Ok(result.Value); + // } + + private int TryGetPageSize() + { + return this.Request.Headers.TryGetValue("x-page-size", out var pageSize) + ? int.Parse(pageSize.FirstOrDefault() ?? string.Empty) + : 10; + } + + private string TryGetPageToken() + { + if (this.Request.Headers.TryGetValue("x-next-page-token", out var nextPageToken)) + { + return nextPageToken; + } + + return null; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberHandler.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberHandler.cs new file mode 100644 index 00000000..059c9a5f --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberHandler.cs @@ -0,0 +1,125 @@ +using CSharpFunctionalExtensions; +using Lab.Sharding.Infrastructure.TraceContext; + +namespace Lab.Sharding.WebAPI.Member; + +public class MemberHandler( + MemberRepository repository, + IContextGetter traceContextGetter, + ILogger logger) +{ + // public async Task> + // InsertAsync(InsertMemberRequest request, + // CancellationToken cancel = default) + // { + // var traceContext = traceContextGetter.Get(); + // var srcMember = await repository.QueryEmailAsync(request.Email, cancel); + // + // //前置條件檢查,可以用 Fluent Pattern 重構 + // var validateResult = Result.Success(srcMember); + // validateResult = ValidateEmail(validateResult, request); + // validateResult = ValidateName(validateResult, request); + // if (validateResult.IsFailure) + // { + // return validateResult; + // } + // + // try + // { + // var count = await repository.InsertAsync(request, cancel); + // var success = Result.Success(srcMember); + // return success; + // + // //發送 Event 給 MQ + // } + // catch (Exception e) + // { + // //各自處理例外,處理過就不要再次 throw + // //模擬插資料失敗 + // var failure = new Failure + // { + // Code = nameof(FailureCode.DbError), + // Message = "資料庫錯誤", + // Data = request, + // Exception = e, + // TraceId = traceContext.TraceId + // }; + // + // logger.LogError($"{failure}", e); + // var failed = Result.Failure(failure); + // return failed; + // } + // } + + // 檢查是否有重複的 Email + private static Result + ValidateEmail(Result previousResult, + InsertMemberRequest dest) + { + if (previousResult.IsFailure) + { + return previousResult; + } + + var src = previousResult.Value; + if (src == null) + { + return Result.Success(src); + } + + if (src.Email == dest.Email) + { + return Result.Failure(new Failure + { + Code = nameof(FailureCode.DuplicateEmail), + Message = "Email 重複", + Data = src, + }); + } + + return Result.Success(src); + } + + // 檢查是否有重複的 Email + private static Result + ValidateName(Result previousResult, + InsertMemberRequest dest) + { + if (previousResult.IsFailure) + { + return previousResult; + } + + var src = previousResult.Value; + if (src == null) + { + return Result.Success(src); + } + + if (src.Email == dest.Email) + { + return Result.Failure(new Failure + { + Code = nameof(FailureCode.DuplicateEmail), + Message = "Email 重複", + Data = src, + }); + } + + return Result.Success(src); + } + + public async Task> + GetMembersAsync(int pageIndex, int pageSize, bool noCache = true, CancellationToken cancel = default) + { + var result = await repository.GetMembersAsync(pageIndex, pageSize, noCache, cancel); + return result; + } + + // public async Task> + // GetMembersAsync(int pageSize, string nextPageToken, bool noCache = true, CancellationToken cancel = default) + // { + // var result = await repository.GetMembersAsync(pageSize, nextPageToken, noCache, cancel); + // return result; + // } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberRepository.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberRepository.cs new file mode 100644 index 00000000..ed221a84 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Member/MemberRepository.cs @@ -0,0 +1,207 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Lab.Sharding.Infrastructure; +using Lab.Sharding.Infrastructure.TraceContext; +using Lab.Sharding.DB; +using Lab.Sharding.DB.AutoGenerated; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Distributed; + +namespace Lab.Sharding.WebAPI.Member; + +public class MemberRepository( + ILogger logger, + IContextGetter contextGetter, + IDbContextFactory dbContextFactory, + IDynamicDbContextFactory dynamicDbContextFactory, + TimeProvider timeProvider, + IUuidProvider uuidProvider, + IDistributedCache cache, + JsonSerializerOptions jsonSerializerOptions) +{ + // public async Task InsertAsync(InsertMemberRequest request, + // CancellationToken cancel = default) + // { + // // throw new DbUpdateConcurrencyException("資料衝突了"); + // + // var now = timeProvider.GetUtcNow(); + // var traceContext = contextGetter.Get(); + // var userId = traceContext.UserId; + // await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancel); + // var toDb = new DB.AutoGenerated.Entities.Member + // { + // Id = uuidProvider.NewId(), + // Name = request.Name, + // Age = request.Age, + // Email = request.Email, + // CreatedAt = now, + // CreatedBy = userId, + // ChangedAt = now, + // ChangedBy = userId + // }; + // var entityEntry = dbContext.Members.Add(toDb); + // return await dbContext.SaveChangesAsync(cancel); + // } + // + // public async Task QueryEmailAsync(string email, + // CancellationToken cancel = default) + // { + // await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancel); + // var query = dbContext.Members + // .Where(p => p.Email == email) + // .Select(p => new Member + // { + // Id = p.Id, + // Name = p.Name, + // Age = p.Age, + // CreatedAt = p.CreatedAt, + // CreatedBy = p.CreatedBy, + // ChangedAt = p.ChangedAt, + // ChangedBy = p.ChangedBy + // }); + // + // var result = await query.TagWith($"{nameof(MemberRepository)}.{nameof(this.QueryEmailAsync)}({email})") + // .AsNoTracking() + // .FirstOrDefaultAsync(cancel); + // return result; + // } + + public async Task> + GetMembersAsync(int pageIndex, int pageSize, bool noCache = false, CancellationToken cancel = default) + { + var traceContext = contextGetter.Get(); + var userId = traceContext.UserId; + PaginatedList result; + var key = nameof(CacheKeys.MemberData); + string cachedData = null; + if (noCache == false) // 如果有快取,就從快取撈資料 + { + cachedData = await cache.GetStringAsync(key, cancel); + if (cachedData != null) + { + result = JsonSerializer.Deserialize>( + cachedData, jsonSerializerOptions); + return result; + } + } + + // await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancel); + await using var dbContext = dynamicDbContextFactory.CreateDbContext("01"); + + var selector = dbContext.Members + .Select(p => new GetMemberResponse { Id = p.Id, Name = p.Name, Age = p.Age, Email = p.Email }) + .AsNoTracking(); + + var totalCount = selector.Count(); + var paging = selector.OrderBy(p => p.Id) + .Skip(pageIndex * pageSize) + .Take(pageSize); + var data = await paging + .TagWith($"{nameof(MemberRepository)}.{nameof(this.GetMembersAsync)}") + .ToListAsync(cancel); + result = new PaginatedList(data, pageIndex, pageSize, totalCount); + cachedData = JsonSerializer.Serialize(result, jsonSerializerOptions); + cache.SetStringAsync(key, cachedData, + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) //最好從組態設定讀取 + }, cancel); + + return result; + } + + // public async Task> + // GetMembersAsync(int pageSize, + // string nextPageToken, + // bool noCache = true, + // CancellationToken cancel = default) + // { + // // if (noCache) 永遠撈新的資料 + // // else 撈快取的資料 + // await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancel); + // var decodeResult = DecodePageToken(nextPageToken); + // var query = dbContext.Members + // .Select(p => p) + // .AsNoTracking(); + // if (decodeResult.lastSequenceId > 0) + // { + // query = query.Where(p => p.SequenceId > decodeResult.lastSequenceId); + // } + // + // query = query.Take(pageSize + 1); + // var selector = + // query.Select(p => new GetMemberResponse + // { + // Id = p.Id, + // Name = p.Name, + // Age = p.Age, + // Email = p.Email, + // SequenceId = p.SequenceId + // }); + // var results = await selector + // .TagWith($"{nameof(MemberRepository)}.{nameof(this.GetMembersAsync)}") + // .ToListAsync(cancel); + // + // // 是否有下一頁 + // var hasNextPage = results.Count > pageSize; + // + // if (hasNextPage) + // { + // // 有下一頁,刪除最後一筆 + // results.RemoveAt(results.Count - 1); + // + // // 產生下一頁的令牌 + // var after = results.LastOrDefault(); + // if (after != null) + // { + // nextPageToken = EncodePageToken(after.Id, after.SequenceId); + // } + // else + // { + // nextPageToken = null; + // } + // } + // + // return new CursorPaginatedList(results, nextPageToken, null); + // } + // + // // 將 Id 和 SequenceId 轉換為下一頁的令牌 + // public static string EncodePageToken(string? lastId, long? lastSequenceId) + // { + // if (lastId == null || lastSequenceId == null) + // { + // return null; + // } + // + // var json = JsonSerializer.Serialize(new { lastId, lastSequenceId }); + // return Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); + // } + // + // // 將下一頁的令牌解碼為 Id 和 SequenceId + // private static (string lastId, long lastSequenceId) DecodePageToken(string nextToken) + // { + // if (string.IsNullOrEmpty(nextToken)) + // { + // return (null, 0); + // } + // + // string lastId = null; + // long lastSequenceId = 0; + // var base64Bytes = Convert.FromBase64String(nextToken); + // var json = Encoding.UTF8.GetString(base64Bytes); + // var jsonNode = JsonNode.Parse(json); + // var jsonObject = jsonNode.AsObject(); + // if (jsonObject.TryGetPropertyValue("lastSequenceId", out var lastSequenceIdNode)) + // { + // lastSequenceId = lastSequenceIdNode.GetValue(); + // } + // + // if (jsonObject.TryGetPropertyValue("lastId", out var lastIdNode)) + // { + // lastId = lastIdNode.GetValue(); + // } + // + // return (lastId, lastSequenceId); + // } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/PaginatedList.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/PaginatedList.cs new file mode 100644 index 00000000..d2e4cca3 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/PaginatedList.cs @@ -0,0 +1,33 @@ +namespace Lab.Sharding.WebAPI; + +enum MyEnum +{ + AA = 0, + BB +} + +public class PaginatedList +{ + public List Items { get; } + + public int PageIndex { get; } + + public int TotalPages { get; } + + public bool HasPreviousPage => PageIndex > 1; + + public bool HasNextPage => PageIndex < TotalPages; + + public PaginatedList() + { + } + + public PaginatedList(List items, int pageIndex, int pageSize, int totalCount) + { + var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); + + Items = items; + PageIndex = pageIndex; + TotalPages = totalPages; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Program.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Program.cs new file mode 100644 index 00000000..908f90ba --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/Program.cs @@ -0,0 +1,158 @@ +using Lab.Sharding.DB; +using Lab.Sharding.DB.AutoGenerated; +using Lab.Sharding.Infrastructure; +using Lab.Sharding.WebAPI; +using Lab.Sharding.WebAPI.Member; +using Scalar.AspNetCore; +using Serilog; +using Serilog.Events; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Hour) + .CreateLogger(); +Log.Information("Starting web host"); + +try +{ + if (Array.FindIndex(args, x => x == "--local") >= 0) + { + var envFolder = EnvironmentUtility.FindParentFolder("env"); + EnvironmentUtility.ReadEnvironmentFile(envFolder, "local.env"); + } + + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + builder.Services.AddSingleton(p => JsonSerializeFactory.DefaultOptions); + builder.Services.AddControllers() + .AddJsonOptions(options => JsonSerializeFactory.Apply(options.JsonSerializerOptions)) + ; + builder.Host + .UseSerilog((context, services, config) => + config.ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console() //正式環境不要用 Console,除非有 Log Provider 專門用來收集 Console Log + .WriteTo.Seq("http://localhost:5341") //log server + .WriteTo.File("logs/aspnet-.txt", rollingInterval: RollingInterval.Minute) //正式環境不要用 File + ); + + // 確定物件都有設定 DI Container + builder.Host.UseDefaultServiceProvider(p => + { + p.ValidateScopes = true; + p.ValidateOnBuild = true; + }); + var configuration = builder.Configuration; + + builder.Services.AddStackExchangeRedisCache((options) => + { + var connectionString = configuration.GetValue(nameof(SYS_REDIS_URL)); + + // options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions + // { + // EndPoints = { connectionString }, + // DefaultDatabase = 0, + // }; + + options.Configuration = connectionString; + + // options.InstanceName = "SampleInstance"; + }); + + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + builder.Services.AddHttpContextAccessor(); + + builder.Services.AddSingleton(_ => TimeProvider.System); + builder.Services.AddContextAccessor(); + builder.Services.AddSysEnvironments(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddExternalApiHttpClient(); + builder.Services.AddDatabase(); + builder.Services.AddScoped, DynamicDbContextFactory>(); + builder.Services.AddSingleton(p => + { + var connectionStringProvider = new ConnectionStringProvider(); + connectionStringProvider.SetConnectionStrings(new Dictionary + { + { + "DatabaseA", + "Server=localhost;Database=Member01;User Id=SA;Password=pass@w0rd1~;TrustServerCertificate=True" + }, + { + "DatabaseB", + "Server=localhost;Database=Member02;User Id=SA;Password=pass@w0rd1~;TrustServerCertificate=True" + } + }); + return connectionStringProvider; + }); + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(options => + options.SwaggerEndpoint("/swagger/v1/swagger.yaml", + "Swagger Demo Documentation v1")); + app.UseReDoc(options => + { + options.DocumentTitle = "Swagger Demo Documentation"; + options.SpecUrl = "/swagger/v1/swagger.yaml"; + options.RoutePrefix = "redoc"; + options.ConfigObject.HideHostname = true; + }); + + app.MapScalarApiReference(p => + { + p.OpenApiRoutePattern = "/swagger/v1/swagger.json"; + + // p.EndpointPathPrefix = "scalar"; + }); + } + + app.UseAuthorization(); + app.UseMiddleware(); + app.MapDefaultControllerRoute(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + //注册Web API Controller + endpoints.MapControllers(); + + //注册MVC Controller模板 {controller=Home}/{action=Index}/{id?} + // endpoints.MapDefaultControllerRoute(); + + //注册健康检查 + // endpoints.MapHealthChecks("/_hc"); + }); + app.UseSerilogRequestLogging(); + app.UseHttpsRedirection(); + app.MapControllers(); + app.Run(); + return 0; +} +catch (Exception ex) +{ + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} + +namespace Lab.Sharding.WebAPI +{ + public partial class Program + { + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/ServiceCollectionExtension.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/ServiceCollectionExtension.cs new file mode 100644 index 00000000..91db5c0e --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/ServiceCollectionExtension.cs @@ -0,0 +1,61 @@ +using Lab.Sharding.Infrastructure.TraceContext; +using Lab.Sharding.DB; +using Lab.Sharding.DB.AutoGenerated; +using Microsoft.EntityFrameworkCore; + +namespace Lab.Sharding.WebAPI; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection AddEnvironments(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } + + public static IServiceCollection AddContextAccessor(this IServiceCollection services) + { + services.AddSingleton>(); + services.AddSingleton>(p => p.GetService>()); + services.AddSingleton>(p => p.GetService>()); + return services; + } + + public static void AddDatabase(this IServiceCollection services) + { + services.AddDbContextFactory((provider, builder) => + { + var environment = provider.GetService(); + var connectionString = environment.Value; + builder.UseSqlServer(connectionString) + .UseLoggerFactory(provider.GetService()) + .EnableSensitiveDataLogging() + ; + }); + } + + public static IHttpClientBuilder AddExternalApiHttpClient(this IServiceCollection services) + { + return services.AddHttpClient("externalApi", + (provider, client) => + { + var traceContext = provider.GetService(); + var externalApi = provider.GetService(); + var traceId = traceContext.TraceId; + client.BaseAddress = new Uri(externalApi.Value); + client.DefaultRequestHeaders.Add(SysHeaderNames.TraceId, traceId); + }) + .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler + { + // 改成 true,會快取 Cookie + UseCookies = false + }); + } + + public static IServiceCollection AddSysEnvironments(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + return services; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/SysHeaderNames.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/SysHeaderNames.cs new file mode 100644 index 00000000..3946d412 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/SysHeaderNames.cs @@ -0,0 +1,6 @@ +namespace Lab.Sharding.WebAPI; + +public abstract class SysHeaderNames +{ + public const string TraceId = "x-trace-id"; +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContext.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContext.cs new file mode 100644 index 00000000..6d1a897e --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContext.cs @@ -0,0 +1,14 @@ +namespace Lab.Sharding.WebAPI; + +public record TraceContext +{ + public string TraceId { get; init; } + + public string UserId { get; init; } + + public Failure SetTraceId(Failure failure) + { + failure.TraceId = this.TraceId; + return failure; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContextMiddleware.cs b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContextMiddleware.cs new file mode 100644 index 00000000..c6aa8327 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/TraceContextMiddleware.cs @@ -0,0 +1,77 @@ +using System.Security.Claims; +using Lab.Sharding.Infrastructure.TraceContext; + +namespace Lab.Sharding.WebAPI; + +public class TraceContextMiddleware +{ + private readonly RequestDelegate _next; + + public TraceContextMiddleware(RequestDelegate next) + { + this._next = next; + } + + public async Task Invoke(HttpContext httpContext, ILogger logger) + { + var traceId = httpContext.Request.Headers[SysHeaderNames.TraceId].FirstOrDefault(); + + //// 若調用端沒有傳入 traceId,則產生一個新的 traceId + if (string.IsNullOrWhiteSpace(traceId)) + { + traceId = httpContext.TraceIdentifier; + } + + // 模擬登入 + Signin(httpContext); + + if (httpContext.User.Identity.IsAuthenticated == false) + { + httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; + await httpContext.Response.WriteAsJsonAsync(new Failure + { + Code = nameof(FailureCode.Unauthorized), + Message = "not login", + }); + return; + } + + var userId = httpContext.User.Identity.Name; + + // 寫入 trace context 到 object context setter + var contextSetter = httpContext.RequestServices.GetService>(); + contextSetter.Set(new TraceContext + { + TraceId = traceId, + UserId = userId + }); + + // 附加 traceId 與 userId 到 log 中 + using var _ = logger.BeginScope("{Location},{TraceId},{UserId}", + "TW", traceId, userId); + + // 附加 traceId 到 response header 中 + IContextGetter? contextGetter = + httpContext.RequestServices.GetService>(); + var traceContext = contextGetter.Get(); + httpContext.Response.Headers.TryAdd(SysHeaderNames.TraceId, traceContext.TraceId); + + await this._next.Invoke(httpContext); + } + + /// + /// 假的登入 + /// + /// + private static void Signin(HttpContext context) + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "yao"), + new Claim(ClaimTypes.Name, "yao"), + }; + var identity = new ClaimsIdentity(claims, "Bearer"); + var principal = new ClaimsPrincipal(identity); + context.User = principal; + } +} \ No newline at end of file diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.Development.json b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.json b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.WebAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.sln b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.sln new file mode 100644 index 00000000..edd1fcdd --- /dev/null +++ b/ORM/EFCore/Lab.Sharding/src/Lab.Sharding.sln @@ -0,0 +1,64 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.IntegrationTest", "Lab.Sharding.IntegrationTest\Lab.Sharding.IntegrationTest.csproj", "{072B154D-149F-416C-AC1A-E009FED7706E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.Infrastructure", "Lab.Sharding.Infrastructure\Lab.Sharding.Infrastructure.csproj", "{F9C2045E-64DE-417A-BCC7-FE20B982153B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.WebAPI", "Lab.Sharding.WebAPI\Lab.Sharding.WebAPI.csproj", "{5BB4C0EB-337D-44F4-BE0A-0694CAF47890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.Testing.Common", "Lab.Sharding.Testing.Common\Lab.Sharding.Testing.Common.csproj", "{6F3E9F7A-8956-4888-A901-80ECDF8D0780}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{1F8C259E-D601-4CCD-BE0D-C1FBB36A1F62}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infra", "Infra", "{9F8A43C9-E365-42E8-B3FA-B217B30C6295}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.DB", "Lab.Sharding.DB\Lab.Sharding.DB.csproj", "{6478BF0B-92D8-458F-B808-3552B60682EA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.Contract", "Lab.Sharding.Contract\Lab.Sharding.Contract.csproj", "{F48CF870-2039-42E2-B4A1-9B0D3F1749FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Sharding.Test", "Lab.Sharding.Test\Lab.Sharding.Test.csproj", "{70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {072B154D-149F-416C-AC1A-E009FED7706E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Release|Any CPU.Build.0 = Release|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Release|Any CPU.Build.0 = Release|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Release|Any CPU.Build.0 = Release|Any CPU + {6F3E9F7A-8956-4888-A901-80ECDF8D0780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F3E9F7A-8956-4888-A901-80ECDF8D0780}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F3E9F7A-8956-4888-A901-80ECDF8D0780}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F3E9F7A-8956-4888-A901-80ECDF8D0780}.Release|Any CPU.Build.0 = Release|Any CPU + {6478BF0B-92D8-458F-B808-3552B60682EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6478BF0B-92D8-458F-B808-3552B60682EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6478BF0B-92D8-458F-B808-3552B60682EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6478BF0B-92D8-458F-B808-3552B60682EA}.Release|Any CPU.Build.0 = Release|Any CPU + {F48CF870-2039-42E2-B4A1-9B0D3F1749FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F48CF870-2039-42E2-B4A1-9B0D3F1749FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F48CF870-2039-42E2-B4A1-9B0D3F1749FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F48CF870-2039-42E2-B4A1-9B0D3F1749FF}.Release|Any CPU.Build.0 = Release|Any CPU + {70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {072B154D-149F-416C-AC1A-E009FED7706E} = {1F8C259E-D601-4CCD-BE0D-C1FBB36A1F62} + {F9C2045E-64DE-417A-BCC7-FE20B982153B} = {9F8A43C9-E365-42E8-B3FA-B217B30C6295} + {6F3E9F7A-8956-4888-A901-80ECDF8D0780} = {1F8C259E-D601-4CCD-BE0D-C1FBB36A1F62} + {6478BF0B-92D8-458F-B808-3552B60682EA} = {9F8A43C9-E365-42E8-B3FA-B217B30C6295} + {F48CF870-2039-42E2-B4A1-9B0D3F1749FF} = {9F8A43C9-E365-42E8-B3FA-B217B30C6295} + {70D7FB0A-9C30-4AD5-A8EE-68B26E351FF9} = {1F8C259E-D601-4CCD-BE0D-C1FBB36A1F62} + 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/Paging/Lab.CursorPaging/.dockerignore b/Paging/Lab.CursorPaging/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/Paging/Lab.CursorPaging/.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/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Controllers/MembersController.cs b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Controllers/MembersController.cs new file mode 100644 index 00000000..4a4ca6cd --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Controllers/MembersController.cs @@ -0,0 +1,211 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Lab.CursorPaging.WebApi.Member.Repository; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Lab.CursorPaging.WebApi.Controllers; + +[ApiController] +public partial class MembersController : ControllerBase +{ + private readonly IDbContextFactory _memberDbContextFactory; + + public MembersController(IDbContextFactory memberDbContextFactory) + { + this._memberDbContextFactory = memberDbContextFactory; + } + + [HttpGet] + [Route("/api/members:page-index")] + public async Task>> GetPageIndex() + { + int pageIndex = this.Request.Headers.TryGetValue("X-Page-Index", out var pages) + ? int.TryParse(pages.FirstOrDefault(), out var parsedPageIndex) + ? parsedPageIndex + : 1 + : 1; + var pageSize = this.TryGetPageSize(); + + await using var dbContext = await this._memberDbContextFactory.CreateDbContextAsync(); + var query = dbContext.Members.Select(p => p); + var totalCount = await dbContext.Members.CountAsync(); + query = query + .Skip((pageIndex - 1) * pageSize) + .Take(pageSize); + query = query.Take(pageSize + 1); + + var results = await query.AsNoTracking().ToListAsync(); + this.Response.Headers.Add("X-Total-Count", totalCount.ToString()); + + return this.Ok(results); + } + + [HttpGet] + [Route("/api/members:cursor")] + public async Task>> GetCursor() + { + var pageSize = this.TryGetPageSize(); + var pageTokenResult = this.TryGetPageToken(); + var lastId = pageTokenResult.LastId; + var lastSequenceId = pageTokenResult.LastSequenceId; + + await using var dbContext = await this._memberDbContextFactory.CreateDbContextAsync(); + var query = dbContext.Members.Select(p => p); + if (string.IsNullOrWhiteSpace(lastId) == false) + { + query = query.Where(p => p.Id.CompareTo(lastId) > 0); + } + + if (lastSequenceId.HasValue) + { + query = query.Where(p => p.SequenceId > lastSequenceId); + } + + query = query.Take(pageSize + 1); + var results = await query.AsNoTracking().ToListAsync(); + + // 是否有下一頁 + bool hasNextPage = results.Count > pageSize; + + if (hasNextPage) + { + // 有下一頁,刪除最後一筆 + results.RemoveAt(results.Count - 1); + + // 產生下一頁的令牌 + var after = results.LastOrDefault(); + if (after != null) + { + var nextToken = EncodePageToken(after.Id, after.SequenceId); + this.Response.Headers.Add("X-Next-Page-Token", nextToken); + } + } + + return this.Ok(results); + } + + [HttpGet] + [Route("/api/members:cursor2")] + public async Task>> GetCursor2() + { + var pageSize = this.TryGetPageSize(); + long? nextPageId = this.Request.Headers.TryGetValue("X-Next-Page-Id", out var data) + ? long.TryParse(data.FirstOrDefault(), out var id) + ? id + : null + : null; + + await using var dbContext = await this._memberDbContextFactory.CreateDbContextAsync(); + var query = dbContext.Members.Select(p => p); + + if (nextPageId.HasValue) + { + query = query.Where(p => p.SequenceId > nextPageId); + } + + query = query.Take(pageSize + 1); + var results = await query.AsNoTracking().ToListAsync(); + + // 是否有下一頁 + bool hasNextPage = results.Count > pageSize; + + if (hasNextPage) + { + // 有下一頁,刪除最後一筆 + results.RemoveAt(results.Count - 1); + + var after = results.LastOrDefault(); + this.Response.Headers.Add("X-Next-Page-Id", after.SequenceId.ToString()); + } + + return this.Ok(results); + } + + private int TryGetPageSize() => + this.Request.Headers.TryGetValue("X-Page-Size", out var sizes) + ? int.Parse(sizes.FirstOrDefault() ?? string.Empty) + : 10; + + private (string? LastId, long? LastSequenceId) TryGetPageToken() + { + if (this.Request.Headers.TryGetValue("X-Next-Page-Token", out var nextToken)) + { + var decodeResult = DecodePageToken(nextToken); + return (decodeResult.lastId, decodeResult.lastSequenceId); + } + + return (null, null); + } + + // 將 Id 和 SequenceId 轉換為下一頁的令牌 + public static string EncodePageToken(string? lastId, long? lastSequenceId) + { + if (lastId == null || lastSequenceId == null) + { + return null; + } + + var json = JsonSerializer.Serialize(new { lastId, lastSequenceId }); + return Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); + } + + // 將下一頁的令牌解碼為 Id 和 SequenceId + public static (string lastId, long lastSequenceId) DecodePageToken(string nextToken) + { + if (string.IsNullOrEmpty(nextToken)) + { + return (null, 0); + } + + string lastId = null; + long lastSequenceId = 0; + var base64Bytes = Convert.FromBase64String(nextToken); + var json = Encoding.UTF8.GetString(base64Bytes); + var jsonNode = JsonNode.Parse(json); + JsonObject jsonObject = jsonNode.AsObject(); + + if (jsonObject.TryGetPropertyValue("lastSequenceId", out var lastSequenceIdNode)) + { + lastSequenceId = lastSequenceIdNode.GetValue(); + } + + if (jsonObject.TryGetPropertyValue("lastId", out var lastIdNode)) + { + lastId = lastIdNode.GetValue(); + } + + return (lastId, lastSequenceId); + } + + [HttpPost] + [Route("/api/members:batch-generate")] + public async Task BatchGenerate() + { + await using var dbContext = await this._memberDbContextFactory.CreateDbContextAsync(); + for (var i = 0; i < 1000; i++) + { + var now = DateTimeOffset.UtcNow; + var member = new MemberDataEntity + { + Id = Guid.NewGuid().ToString(), + Name = Faker.Name.FullName(), + Age = DateTime.Now.Year - Faker.Date.Birthday().Year, + Email = Faker.User.Email(), + Phone = Faker.Phone.GetPhoneNumber(), + Address = Faker.Address.SecondaryAddress(), + CreatedAt = now, + CreatedBy = "sys", + UpdatedAt = now, + UpdatedBy = "sys", + }; + + dbContext.Members.Add(member); + } + + var count = await dbContext.SaveChangesAsync(); + + return this.NoContent(); + } +} \ No newline at end of file diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Dockerfile b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Dockerfile new file mode 100644 index 00000000..43c017dd --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Lab.CursorPaging.WebApi/Lab.CursorPaging.WebApi.csproj", "Lab.CursorPaging.WebApi/"] +RUN dotnet restore "Lab.CursorPaging.WebApi/Lab.CursorPaging.WebApi.csproj" +COPY . . +WORKDIR "/src/Lab.CursorPaging.WebApi" +RUN dotnet build "Lab.CursorPaging.WebApi.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "Lab.CursorPaging.WebApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Lab.CursorPaging.WebApi.dll"] diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Lab.CursorPaging.WebApi.csproj b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Lab.CursorPaging.WebApi.csproj new file mode 100644 index 00000000..f9a63c51 --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Lab.CursorPaging.WebApi.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + true + Linux + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + .dockerignore + + + + diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Lab.CursorPaging.WebApi.http b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Lab.CursorPaging.WebApi.http new file mode 100644 index 00000000..224f87c1 --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Lab.CursorPaging.WebApi.http @@ -0,0 +1,6 @@ +@Lab.CursorPaging.WebApi_HostAddress = http://localhost:5164 + +GET {{Lab.CursorPaging.WebApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Member/Repository/MemberDbContext.cs b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Member/Repository/MemberDbContext.cs new file mode 100644 index 00000000..23e81f42 --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Member/Repository/MemberDbContext.cs @@ -0,0 +1,73 @@ +using Microsoft.EntityFrameworkCore; + +namespace Lab.CursorPaging.WebApi.Member.Repository; + +public class MemberDbContext : DbContext +{ + private static readonly bool[] s_migrated = { false }; + + public MemberDbContext(DbContextOptions options) : base(options) + { + if (!s_migrated[0]) + { + lock (s_migrated) + { + if (!s_migrated[0]) + { + this.Database.Migrate(); + s_migrated[0] = true; + } + } + } + } + + public DbSet Members { get; set; } = default!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(builder => + { + //property + builder.ToTable("Member"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).IsRequired(); + builder.Property(x => x.Age).IsRequired(); + builder.Property(x => x.CreatedAt).IsRequired(); + builder.Property(x => x.CreatedBy).IsRequired(); + builder.Property(x => x.UpdatedAt).IsRequired(); + builder.Property(x => x.UpdatedBy).IsRequired(); + builder.Property(p => p.SequenceId).ValueGeneratedOnAdd(); + + //index + builder.HasIndex(x => x.SequenceId).IsUnique(); + }); + } +} + +public class MemberDataEntity +{ + public string Id { get; set; } + + public string Name { get; set; } = default!; + + public int Age { get; set; } + + public string? Email { get; set; } + + public string? Phone { get; set; } + + public string? Address { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public DateTimeOffset UpdatedAt { get; set; } + + public string UpdatedBy { get; set; } + + // [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long SequenceId { get; set; } +} \ No newline at end of file diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Migrations/20240107142909_InitialCreate.Designer.cs b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Migrations/20240107142909_InitialCreate.Designer.cs new file mode 100644 index 00000000..67614025 --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Migrations/20240107142909_InitialCreate.Designer.cs @@ -0,0 +1,79 @@ +// +using System; +using Lab.CursorPaging.WebApi.Member.Repository; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Lab.CursorPaging.WebApi.Migrations +{ + [DbContext(typeof(MemberDbContext))] + [Migration("20240107142909_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Lab.CursorPaging.WebApi.Member.Repository.MemberDataEntity", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Age") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("SequenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceId")); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SequenceId") + .IsUnique(); + + b.ToTable("Member", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Migrations/20240107142909_InitialCreate.cs b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Migrations/20240107142909_InitialCreate.cs new file mode 100644 index 00000000..bd56945c --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Migrations/20240107142909_InitialCreate.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Lab.CursorPaging.WebApi.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Member", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Age = table.Column(type: "integer", nullable: false), + Email = table.Column(type: "text", nullable: true), + Phone = table.Column(type: "text", nullable: true), + Address = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "text", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedBy = table.Column(type: "text", nullable: false), + SequenceId = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + }, + constraints: table => + { + table.PrimaryKey("PK_Member", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Member_SequenceId", + table: "Member", + column: "SequenceId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Member"); + } + } +} diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Migrations/MemberDbContextModelSnapshot.cs b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Migrations/MemberDbContextModelSnapshot.cs new file mode 100644 index 00000000..39b91195 --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Migrations/MemberDbContextModelSnapshot.cs @@ -0,0 +1,76 @@ +// +using System; +using Lab.CursorPaging.WebApi.Member.Repository; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Lab.CursorPaging.WebApi.Migrations +{ + [DbContext(typeof(MemberDbContext))] + partial class MemberDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Lab.CursorPaging.WebApi.Member.Repository.MemberDataEntity", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Age") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("SequenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceId")); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SequenceId") + .IsUnique(); + + b.ToTable("Member", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Program.cs b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Program.cs new file mode 100644 index 00000000..43c95398 --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Program.cs @@ -0,0 +1,43 @@ +using Lab.CursorPaging.WebApi.Member.Repository; +using Microsoft.EntityFrameworkCore; + +Environment.SetEnvironmentVariable("DbConnectionString", "Host=localhost;Port=5432;Database=Member;Username=postgres;Password=guest;"); +Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddDbContextFactory((sp, options) => +{ + // var connProvider = sp.GetRequiredService(); + var connString = Environment.GetEnvironmentVariable("DbConnectionString"); + options + .UseNpgsql( + connString, + builder => builder.EnableRetryOnFailure( + 10, + TimeSpan.FromSeconds(30), + new List { "57P01" })) + .UseLoggerFactory(sp.GetService()) + ; +}); +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +// app.UseHttpsRedirection(); +app.UseRouting(); +app.MapControllers(); + +app.Run(); + +public partial class Program { } \ No newline at end of file diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Properties/launchSettings.json b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Properties/launchSettings.json new file mode 100644 index 00000000..0247862f --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:4298", + "sslPort": 44369 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5164", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7293;http://localhost:5164", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/appsettings.Development.json b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/appsettings.json b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.WebApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Paging/Lab.CursorPaging/Lab.CursorPaging.sln b/Paging/Lab.CursorPaging/Lab.CursorPaging.sln new file mode 100644 index 00000000..bb149dda --- /dev/null +++ b/Paging/Lab.CursorPaging/Lab.CursorPaging.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.CursorPaging.WebApi", "Lab.CursorPaging.WebApi\Lab.CursorPaging.WebApi.csproj", "{764FE9E4-9198-4812-A099-0AE48FE0816A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{735C1FFE-0C51-436D-8A8C-27CA1E7DF20C}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject1", "TestProject1\TestProject1.csproj", "{1543BF4E-385C-43A5-85F7-92A357078549}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {764FE9E4-9198-4812-A099-0AE48FE0816A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {764FE9E4-9198-4812-A099-0AE48FE0816A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {764FE9E4-9198-4812-A099-0AE48FE0816A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {764FE9E4-9198-4812-A099-0AE48FE0816A}.Release|Any CPU.Build.0 = Release|Any CPU + {1543BF4E-385C-43A5-85F7-92A357078549}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1543BF4E-385C-43A5-85F7-92A357078549}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1543BF4E-385C-43A5-85F7-92A357078549}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1543BF4E-385C-43A5-85F7-92A357078549}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Paging/Lab.CursorPaging/docker-compose.yml b/Paging/Lab.CursorPaging/docker-compose.yml new file mode 100644 index 00000000..a760b5eb --- /dev/null +++ b/Paging/Lab.CursorPaging/docker-compose.yml @@ -0,0 +1,8 @@ +services: + db: + image: postgres + container_name: postgres-latest + environment: + - POSTGRES_PASSWORD=guest + ports: + - 5432:5432 \ No newline at end of file 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/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis.sln b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis.sln new file mode 100644 index 00000000..fcb3aa2f --- /dev/null +++ b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ClientRateLimitAndRedis", "Lab.ClientRateLimitAndRedis\Lab.ClientRateLimitAndRedis.csproj", "{04DE6CBF-1595-4859-9F4D-B46AE2A7712B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A0A316CD-39E6-4456-96D7-4B6EB1D491D5}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.ClientRateLimitAndRedis2", "Lab.ClientRateLimitAndRedis2\Lab.ClientRateLimitAndRedis2.csproj", "{C9E118ED-55B0-43CD-A87C-6CD138ADE088}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {04DE6CBF-1595-4859-9F4D-B46AE2A7712B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04DE6CBF-1595-4859-9F4D-B46AE2A7712B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04DE6CBF-1595-4859-9F4D-B46AE2A7712B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04DE6CBF-1595-4859-9F4D-B46AE2A7712B}.Release|Any CPU.Build.0 = Release|Any CPU + {C9E118ED-55B0-43CD-A87C-6CD138ADE088}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9E118ED-55B0-43CD-A87C-6CD138ADE088}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9E118ED-55B0-43CD-A87C-6CD138ADE088}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9E118ED-55B0-43CD-A87C-6CD138ADE088}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/ClientSideRateLimitedHandler.cs b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/ClientSideRateLimitedHandler.cs new file mode 100644 index 00000000..4b6ab362 --- /dev/null +++ b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/ClientSideRateLimitedHandler.cs @@ -0,0 +1,50 @@ +using System.Globalization; +using System.Net; +using System.Threading.RateLimiting; + +namespace Lab.ClientRateLimitAndRedis; + +internal sealed class ClientSideRateLimitedHandler(RateLimiter limiter) + : DelegatingHandler(new HttpClientHandler()), IAsyncDisposable +{ + protected override async Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + using var lease = await limiter.AcquireAsync( + permitCount: 1, cancellationToken); + if (lease.IsAcquired) + { + return await base.SendAsync(request, cancellationToken); + } + + var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests); + if (lease.TryGetMetadata( + MetadataName.RetryAfter, out TimeSpan retryAfter)) + { + response.Headers.Add( + "Retry-After", + ((int)retryAfter.TotalSeconds).ToString( + NumberFormatInfo.InvariantInfo)); + } + + return response; + } + + async ValueTask IAsyncDisposable.DisposeAsync() + { + await limiter.DisposeAsync().ConfigureAwait(false); + + this.Dispose(disposing: false); + GC.SuppressFinalize(this); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + limiter.Dispose(); + } + } +} \ No newline at end of file diff --git a/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis.csproj b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis.csproj new file mode 100644 index 00000000..c4c12ea9 --- /dev/null +++ b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + Linux + + + + + + + + diff --git a/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/Program.cs b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/Program.cs new file mode 100644 index 00000000..bae82fbe --- /dev/null +++ b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/Program.cs @@ -0,0 +1,85 @@ +using Lab.ClientRateLimitAndRedis; +using RedisRateLimiting; +using StackExchange.Redis; + +var redisTokenBucketRateLimiter = new RedisTokenBucketRateLimiter("demo-redis-token-bucket", + new RedisTokenBucketRateLimiterOptions + { + ConnectionMultiplexerFactory = () => ConnectionMultiplexer.Connect("localhost"), + TokenLimit = 50, + TokensPerPeriod = 50, + ReplenishmentPeriod = TimeSpan.FromSeconds(10) + } +); + +var redisSlidingWindowRateLimiter = new RedisSlidingWindowRateLimiter("demo-redis-sliding-window", + new RedisSlidingWindowRateLimiterOptions + { + ConnectionMultiplexerFactory = () => ConnectionMultiplexer.Connect("localhost"), + Window = TimeSpan.FromSeconds(10), + PermitLimit = 50 + } +); +var redisFixedWindowRateLimiter = new RedisFixedWindowRateLimiter("demo-redis-fixed-window", + new RedisFixedWindowRateLimiterOptions + { + ConnectionMultiplexerFactory = () => ConnectionMultiplexer.Connect("localhost"), + Window = TimeSpan.FromSeconds(10), + PermitLimit = 50 + } +); + +// Create an HTTP client with the client-side rate limited handler. +var limiter = redisSlidingWindowRateLimiter; + +using HttpClient client = new( + handler: new ClientSideRateLimitedHandler(limiter: limiter)); + +Console.WriteLine($"{DateTime.Now.ToString()},Start"); + +var count = 0; +while (true) +{ + var lease = await limiter.AcquireAsync(permitCount: 1, cancellationToken: default); + if (lease.IsAcquired == false) + { + Console.WriteLine("Rate limit exceeded. Pausing requests for 1 sec."); + await Task.Delay(TimeSpan.FromSeconds(1)); + continue; + } + + var tasks = new List() + { + Task.Run(async () => { await Task.Delay(TimeSpan.FromMilliseconds(100)); }), + Task.Run(async () => { await Task.Delay(TimeSpan.FromMilliseconds(120)); }), + }; + await Task.WhenAll(tasks); + count++; + Console.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss}, Run Count: {count}"); +} + +var oneHundredUrls = Enumerable.Range(0, 100).Select( + i => $"https://example.com?iteration={i:0#}"); + +// Flood the HTTP client with requests. +var floodOneThroughFortyNineTask = Parallel.ForEachAsync( + source: oneHundredUrls.Take(0..49), + body: (url, cancellationToken) => GetAsync(client, url, cancellationToken)); + +var floodFiftyThroughOneHundredTask = Parallel.ForEachAsync( + source: oneHundredUrls.Take(^50..), + body: (url, cancellationToken) => GetAsync(client, url, cancellationToken)); + +await Task.WhenAll( + floodOneThroughFortyNineTask, + floodFiftyThroughOneHundredTask); + +static async ValueTask GetAsync( + HttpClient client, string url, CancellationToken cancellationToken) +{ + using var response = + await client.GetAsync(url, cancellationToken); + + Console.WriteLine( + $"URL: {url}, HTTP status code: {response.StatusCode} ({(int)response.StatusCode})"); +} \ No newline at end of file diff --git a/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/RateLimitHttpMessageHandler.cs b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/RateLimitHttpMessageHandler.cs new file mode 100644 index 00000000..29193862 --- /dev/null +++ b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis/RateLimitHttpMessageHandler.cs @@ -0,0 +1,58 @@ +namespace Lab.ClientRateLimitAndRedis; + +public class RateLimitHttpMessageHandler : DelegatingHandler +{ + private readonly List _callLog = + new List(); + private readonly TimeSpan _limitTime; + private readonly int _limitCount; + + public RateLimitHttpMessageHandler(int limitCount, TimeSpan limitTime) + { + this._limitCount = limitCount; + this._limitTime = limitTime; + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var now = DateTimeOffset.UtcNow; + + lock (this._callLog) + { + this._callLog.Add(now); + + while (this._callLog.Count > this._limitCount) + this._callLog.RemoveAt(0); + } + + await this.LimitDelay(now); + + return await base.SendAsync(request, cancellationToken); + } + + private async Task LimitDelay(DateTimeOffset now) + { + if (this._callLog.Count < this._limitCount) + return; + + var limit = now.Add(-this._limitTime); + + var lastCall = DateTimeOffset.MinValue; + var shouldLock = false; + + lock (this._callLog) + { + lastCall = this._callLog.FirstOrDefault(); + shouldLock = this._callLog.Count(x => x >= limit) >= this._limitCount; + } + + var delayTime = shouldLock && (lastCall > DateTimeOffset.MinValue) + ? (limit - lastCall) + : TimeSpan.Zero; + + if (delayTime > TimeSpan.Zero) + await Task.Delay(delayTime); + } +} \ No newline at end of file diff --git a/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/ClientSideRateLimitedHandler.cs b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/ClientSideRateLimitedHandler.cs new file mode 100644 index 00000000..e7204064 --- /dev/null +++ b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/ClientSideRateLimitedHandler.cs @@ -0,0 +1,50 @@ +using System.Globalization; +using System.Net; +using System.Threading.RateLimiting; + +namespace Lab.ClientRateLimitAndRedis2; + +internal sealed class ClientSideRateLimitedHandler(RateLimiter limiter) + : DelegatingHandler(new HttpClientHandler()), IAsyncDisposable +{ + protected override async Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + using var lease = await limiter.AcquireAsync( + permitCount: 1, cancellationToken); + if (lease.IsAcquired) + { + return await base.SendAsync(request, cancellationToken); + } + + var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests); + if (lease.TryGetMetadata( + MetadataName.RetryAfter, out TimeSpan retryAfter)) + { + response.Headers.Add( + "Retry-After", + ((int)retryAfter.TotalSeconds).ToString( + NumberFormatInfo.InvariantInfo)); + } + + return response; + } + + async ValueTask IAsyncDisposable.DisposeAsync() + { + await limiter.DisposeAsync().ConfigureAwait(false); + + this.Dispose(disposing: false); + GC.SuppressFinalize(this); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + limiter.Dispose(); + } + } +} \ No newline at end of file diff --git a/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/Lab.ClientRateLimitAndRedis2.csproj b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/Lab.ClientRateLimitAndRedis2.csproj new file mode 100644 index 00000000..c4c12ea9 --- /dev/null +++ b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/Lab.ClientRateLimitAndRedis2.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + Linux + + + + + + + + diff --git a/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/Program.cs b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/Program.cs new file mode 100644 index 00000000..92e130be --- /dev/null +++ b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/Program.cs @@ -0,0 +1,85 @@ +using Lab.ClientRateLimitAndRedis2; +using RedisRateLimiting; +using StackExchange.Redis; + +var redisTokenBucketRateLimiter = new RedisTokenBucketRateLimiter("demo-redis-token-bucket", + new RedisTokenBucketRateLimiterOptions + { + ConnectionMultiplexerFactory = () => ConnectionMultiplexer.Connect("localhost"), + TokenLimit = 50, + TokensPerPeriod = 50, + ReplenishmentPeriod = TimeSpan.FromSeconds(10) + } +); + +var redisSlidingWindowRateLimiter = new RedisSlidingWindowRateLimiter("demo-redis-sliding-window", + new RedisSlidingWindowRateLimiterOptions + { + ConnectionMultiplexerFactory = () => ConnectionMultiplexer.Connect("localhost"), + Window = TimeSpan.FromSeconds(10), + PermitLimit = 50 + } +); +var redisFixedWindowRateLimiter = new RedisFixedWindowRateLimiter("demo-redis-fixed-window", + new RedisFixedWindowRateLimiterOptions + { + ConnectionMultiplexerFactory = () => ConnectionMultiplexer.Connect("localhost"), + Window = TimeSpan.FromSeconds(10), + PermitLimit = 50 + } +); + +// Create an HTTP client with the client-side rate limited handler. +var limiter = redisSlidingWindowRateLimiter; + +using HttpClient client = new( + handler: new ClientSideRateLimitedHandler(limiter: limiter)); + +Console.WriteLine($"{DateTime.Now.ToString()},Start"); + +var count = 0; +while (true) +{ + var lease = await limiter.AcquireAsync(permitCount: 1, cancellationToken: default); + if (lease.IsAcquired == false) + { + Console.WriteLine("Rate limit exceeded. Pausing requests for 1 sec."); + await Task.Delay(TimeSpan.FromSeconds(1)); + continue; + } + + var tasks = new List() + { + Task.Run(async () => { await Task.Delay(TimeSpan.FromMilliseconds(100)); }), + Task.Run(async () => { await Task.Delay(TimeSpan.FromMilliseconds(120)); }), + }; + await Task.WhenAll(tasks); + count++; + Console.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss}, Run Count: {count}"); +} + +var oneHundredUrls = Enumerable.Range(0, 100).Select( + i => $"https://example.com?iteration={i:0#}"); + +// Flood the HTTP client with requests. +var floodOneThroughFortyNineTask = Parallel.ForEachAsync( + source: oneHundredUrls.Take(0..49), + body: (url, cancellationToken) => GetAsync(client, url, cancellationToken)); + +var floodFiftyThroughOneHundredTask = Parallel.ForEachAsync( + source: oneHundredUrls.Take(^50..), + body: (url, cancellationToken) => GetAsync(client, url, cancellationToken)); + +await Task.WhenAll( + floodOneThroughFortyNineTask, + floodFiftyThroughOneHundredTask); + +static async ValueTask GetAsync( + HttpClient client, string url, CancellationToken cancellationToken) +{ + using var response = + await client.GetAsync(url, cancellationToken); + + Console.WriteLine( + $"URL: {url}, HTTP status code: {response.StatusCode} ({(int)response.StatusCode})"); +} \ No newline at end of file diff --git a/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/RateLimitHttpMessageHandler.cs b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/RateLimitHttpMessageHandler.cs new file mode 100644 index 00000000..3871cb29 --- /dev/null +++ b/Rate Limit/Lab.ClientRateLimitAndRedis/Lab.ClientRateLimitAndRedis2/RateLimitHttpMessageHandler.cs @@ -0,0 +1,58 @@ +namespace Lab.ClientRateLimitAndRedis2; + +public class RateLimitHttpMessageHandler : DelegatingHandler +{ + private readonly List _callLog = + new List(); + private readonly TimeSpan _limitTime; + private readonly int _limitCount; + + public RateLimitHttpMessageHandler(int limitCount, TimeSpan limitTime) + { + this._limitCount = limitCount; + this._limitTime = limitTime; + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var now = DateTimeOffset.UtcNow; + + lock (this._callLog) + { + this._callLog.Add(now); + + while (this._callLog.Count > this._limitCount) + this._callLog.RemoveAt(0); + } + + await this.LimitDelay(now); + + return await base.SendAsync(request, cancellationToken); + } + + private async Task LimitDelay(DateTimeOffset now) + { + if (this._callLog.Count < this._limitCount) + return; + + var limit = now.Add(-this._limitTime); + + var lastCall = DateTimeOffset.MinValue; + var shouldLock = false; + + lock (this._callLog) + { + lastCall = this._callLog.FirstOrDefault(); + shouldLock = this._callLog.Count(x => x >= limit) >= this._limitCount; + } + + var delayTime = shouldLock && (lastCall > DateTimeOffset.MinValue) + ? (limit - lastCall) + : TimeSpan.Zero; + + if (delayTime > TimeSpan.Zero) + await Task.Delay(delayTime); + } +} \ No newline at end of file diff --git a/Rate Limit/Lab.ClientRateLimitAndRedis/docker-compose.yml b/Rate Limit/Lab.ClientRateLimitAndRedis/docker-compose.yml new file mode 100644 index 00000000..ffdbef3b --- /dev/null +++ b/Rate Limit/Lab.ClientRateLimitAndRedis/docker-compose.yml @@ -0,0 +1,15 @@ +services: + redis: + image: redis + ports: + - 6379:6379 + +# # 在登入頁面 +# # host:redis +# # port:6379 +# redis-admin: +# image: marian/rebrow +# ports: +# - 5001:5001 +# depends_on: +# - redis \ No newline at end of file diff --git a/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/ClientSideRateLimitedHandler.cs b/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/ClientSideRateLimitedHandler.cs new file mode 100644 index 00000000..26e19488 --- /dev/null +++ b/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/ClientSideRateLimitedHandler.cs @@ -0,0 +1,50 @@ +using System.Globalization; +using System.Net; +using System.Threading.RateLimiting; + +namespace Lab.HttpClientLimit; + +internal sealed class ClientSideRateLimitedHandler(RateLimiter limiter) + : DelegatingHandler(new HttpClientHandler()), IAsyncDisposable +{ + protected override async Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + using var lease = await limiter.AcquireAsync( + permitCount: 1, cancellationToken); + if (lease.IsAcquired) + { + return await base.SendAsync(request, cancellationToken); + } + + var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests); + if (lease.TryGetMetadata( + MetadataName.RetryAfter, out TimeSpan retryAfter)) + { + response.Headers.Add( + "Retry-After", + ((int)retryAfter.TotalSeconds).ToString( + NumberFormatInfo.InvariantInfo)); + } + + return response; + } + + async ValueTask IAsyncDisposable.DisposeAsync() + { + await limiter.DisposeAsync().ConfigureAwait(false); + + Dispose(disposing: false); + GC.SuppressFinalize(this); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + limiter.Dispose(); + } + } +} \ No newline at end of file diff --git a/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/Lab.HttpClientLimit.csproj b/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/Lab.HttpClientLimit.csproj new file mode 100644 index 00000000..70997f62 --- /dev/null +++ b/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/Lab.HttpClientLimit.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/Program.cs b/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/Program.cs new file mode 100644 index 00000000..2d2349ba --- /dev/null +++ b/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/Program.cs @@ -0,0 +1,84 @@ +using System.Threading.RateLimiting; +using Lab.HttpClientLimit; + +var tokenBucketRateLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions +{ + TokenLimit = 1000, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 1000, + ReplenishmentPeriod = TimeSpan.FromSeconds(1), + TokensPerPeriod = 17, + AutoReplenishment = true +}); +var slidingWindowRateLimiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions +{ + Window = TimeSpan.FromSeconds(10), + SegmentsPerWindow = 100, + AutoReplenishment = true, + PermitLimit = 10, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 1 +}); +var fixedWindowRateLimiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions +{ + Window = TimeSpan.FromSeconds(10), + AutoReplenishment = true, + PermitLimit = 10, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 1 +}); + +// Create an HTTP client with the client-side rate limited handler. +var limiter = fixedWindowRateLimiter; + +using HttpClient client = new( + handler: new ClientSideRateLimitedHandler(limiter: limiter)); + +Console.WriteLine($"{DateTime.Now.ToString()},Start"); + +var count = 0; +while (false) +{ + var lease = await limiter.AcquireAsync(permitCount: 1, cancellationToken: default); + if (lease.IsAcquired == false) + { + Console.WriteLine("Rate limit exceeded. Pausing requests for 1 minute."); + await Task.Delay(TimeSpan.FromMinutes(1)); + continue; + } + + var tasks = new List() + { + Task.Run(async () => { await Task.Delay(TimeSpan.FromMilliseconds(100)); }), + Task.Run(async () => { await Task.Delay(TimeSpan.FromMilliseconds(120)); }), + }; + await Task.WhenAll(tasks); + count++; + Console.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss}, Run Count: {count}"); +} + +var oneHundredUrls = Enumerable.Range(0, 100).Select( + i => $"https://example.com?iteration={i:0#}"); + +// Flood the HTTP client with requests. +var floodOneThroughFortyNineTask = Parallel.ForEachAsync( + source: oneHundredUrls.Take(0..49), + body: (url, cancellationToken) => GetAsync(client, url, cancellationToken)); + +var floodFiftyThroughOneHundredTask = Parallel.ForEachAsync( + source: oneHundredUrls.Take(^50..), + body: (url, cancellationToken) => GetAsync(client, url, cancellationToken)); + +await Task.WhenAll( + floodOneThroughFortyNineTask, + floodFiftyThroughOneHundredTask); + +static async ValueTask GetAsync( + HttpClient client, string url, CancellationToken cancellationToken) +{ + using var response = + await client.GetAsync(url, cancellationToken); + + Console.WriteLine( + $"URL: {url}, HTTP status code: {response.StatusCode} ({(int)response.StatusCode})"); +} \ No newline at end of file diff --git a/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/RateLimitHttpMessageHandler.cs b/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/RateLimitHttpMessageHandler.cs new file mode 100644 index 00000000..c48080e7 --- /dev/null +++ b/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientLimit/RateLimitHttpMessageHandler.cs @@ -0,0 +1,58 @@ +namespace Lab.HttpClientLimit; + +public class RateLimitHttpMessageHandler : DelegatingHandler +{ + private readonly List _callLog = + new List(); + private readonly TimeSpan _limitTime; + private readonly int _limitCount; + + public RateLimitHttpMessageHandler(int limitCount, TimeSpan limitTime) + { + _limitCount = limitCount; + _limitTime = limitTime; + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var now = DateTimeOffset.UtcNow; + + lock (_callLog) + { + _callLog.Add(now); + + while (_callLog.Count > _limitCount) + _callLog.RemoveAt(0); + } + + await LimitDelay(now); + + return await base.SendAsync(request, cancellationToken); + } + + private async Task LimitDelay(DateTimeOffset now) + { + if (_callLog.Count < _limitCount) + return; + + var limit = now.Add(-_limitTime); + + var lastCall = DateTimeOffset.MinValue; + var shouldLock = false; + + lock (_callLog) + { + lastCall = _callLog.FirstOrDefault(); + shouldLock = _callLog.Count(x => x >= limit) >= _limitCount; + } + + var delayTime = shouldLock && (lastCall > DateTimeOffset.MinValue) + ? (limit - lastCall) + : TimeSpan.Zero; + + if (delayTime > TimeSpan.Zero) + await Task.Delay(delayTime); + } +} \ No newline at end of file diff --git a/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientRateLimit.sln b/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientRateLimit.sln new file mode 100644 index 00000000..e229aa88 --- /dev/null +++ b/Rate Limit/Lab.HttpClientRateLimit/Lab.HttpClientRateLimit.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.HttpClientLimit", "Lab.HttpClientLimit\Lab.HttpClientLimit.csproj", "{04DE6CBF-1595-4859-9F4D-B46AE2A7712B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {04DE6CBF-1595-4859-9F4D-B46AE2A7712B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04DE6CBF-1595-4859-9F4D-B46AE2A7712B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04DE6CBF-1595-4859-9F4D-B46AE2A7712B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04DE6CBF-1595-4859-9F4D-B46AE2A7712B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Controllers/WeatherForecastController.cs b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..27d03e37 --- /dev/null +++ b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.MsRateLimit.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 = DateOnly.FromDateTime(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/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Lab.MsRateLimit.WebAPI.csproj b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Lab.MsRateLimit.WebAPI.csproj new file mode 100644 index 00000000..f2ab5566 --- /dev/null +++ b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Lab.MsRateLimit.WebAPI.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + + + + + + + + diff --git a/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Program.cs b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Program.cs new file mode 100644 index 00000000..329fe361 --- /dev/null +++ b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Program.cs @@ -0,0 +1,26 @@ +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 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/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Properties/launchSettings.json b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Properties/launchSettings.json new file mode 100644 index 00000000..8446254f --- /dev/null +++ b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:4451", + "sslPort": 44381 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5293", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7249;http://localhost:5293", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/WeatherForecast.cs b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/WeatherForecast.cs new file mode 100644 index 00000000..f1024e56 --- /dev/null +++ b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Lab.MsRateLimit.WebAPI; + +public class WeatherForecast +{ + public DateOnly 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/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/appsettings.Development.json b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/appsettings.json b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.WebAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.sln b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.sln new file mode 100644 index 00000000..7c579b58 --- /dev/null +++ b/Rate Limit/Lab.MsRateLimit/Lab.MsRateLimit.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.MsRateLimit.WebAPI", "Lab.MsRateLimit.WebAPI\Lab.MsRateLimit.WebAPI.csproj", "{1F2989FC-AAD0-480B-B2FB-5F5D6DE91053}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject1", "TestProject1\TestProject1.csproj", "{11E81AE5-8F85-4649-B747-EF0E196B9C05}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1F2989FC-AAD0-480B-B2FB-5F5D6DE91053}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F2989FC-AAD0-480B-B2FB-5F5D6DE91053}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F2989FC-AAD0-480B-B2FB-5F5D6DE91053}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F2989FC-AAD0-480B-B2FB-5F5D6DE91053}.Release|Any CPU.Build.0 = Release|Any CPU + {11E81AE5-8F85-4649-B747-EF0E196B9C05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11E81AE5-8F85-4649-B747-EF0E196B9C05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11E81AE5-8F85-4649-B747-EF0E196B9C05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11E81AE5-8F85-4649-B747-EF0E196B9C05}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Rate Limit/Lab.MsRateLimit/TestProject1/UnitTest1.cs b/Rate Limit/Lab.MsRateLimit/TestProject1/UnitTest1.cs new file mode 100644 index 00000000..f779f51b --- /dev/null +++ b/Rate Limit/Lab.MsRateLimit/TestProject1/UnitTest1.cs @@ -0,0 +1,33 @@ +using System.Threading.RateLimiting; + +namespace TestProject1; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public async Task TestMethod1() + { + RateLimiter limiter = new ConcurrencyLimiter( + new ConcurrencyLimiterOptions + { + PermitLimit = 2, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 2, + }); + limiter.ConfigureAwait(false); + var acquireResult1 = limiter.AcquireAsync(permitCount: 2).Result; + var acquireResult2 = limiter.AcquireAsync(permitCount: 2).Result; + // thread 1 + // var acquireResult1 = await limiter.AcquireAsync(permitCount: 2); + // if (acquireResult1.IsAcquired) + // { + // } + // + // // thread 2 + // var acquireResult2 = await limiter.AcquireAsync(permitCount: 2); + // if (acquireResult2.IsAcquired) + // { + // } + } +} \ No newline at end of file diff --git a/Redis/Lab.Redis.Client/Taskfile.yml b/Redis/Lab.Redis.Client/Taskfile.yml new file mode 100644 index 00000000..0a12096f --- /dev/null +++ b/Redis/Lab.Redis.Client/Taskfile.yml @@ -0,0 +1,11 @@ +# Taskfile.yml + +version: "3" + +dotenv: [ "secrets/secrets.env" ] + +tasks: + dev-stop: + desc: Stop development environment + cmds: + - docker-compose down \ No newline at end of file diff --git a/Redis/Lab.Redis.Client/docker-compose.yml b/Redis/Lab.Redis.Client/docker-compose.yml new file mode 100644 index 00000000..af57f699 --- /dev/null +++ b/Redis/Lab.Redis.Client/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3.8" + +services: + redis: + image: redis + ports: + - 6379:6379 + + # 在登入頁面 + # host:redis + # port:6379 + redis-admin: + image: marian/rebrow + ports: + - 5001:5001 + depends_on: + - redis \ No newline at end of file diff --git a/Redis/Lab.Redis.Client/src/Lab.Redis.Client.sln b/Redis/Lab.Redis.Client/src/Lab.Redis.Client.sln new file mode 100644 index 00000000..587b7ba4 --- /dev/null +++ b/Redis/Lab.Redis.Client/src/Lab.Redis.Client.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject1", "TestProject1\TestProject1.csproj", "{8339C08B-EA55-4705-BA65-BA93ED6195DB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Redis.Client", "Lab.Redis.Client\Lab.Redis.Client.csproj", "{27A19D5D-9E2F-4B8A-9516-2FFD77B1052B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{2CC81745-AD2C-41A5-B7CB-5E2691440BAD}" + ProjectSection(SolutionItems) = preProject + ..\Taskfile.yml = ..\Taskfile.yml + ..\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 + {8339C08B-EA55-4705-BA65-BA93ED6195DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8339C08B-EA55-4705-BA65-BA93ED6195DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8339C08B-EA55-4705-BA65-BA93ED6195DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8339C08B-EA55-4705-BA65-BA93ED6195DB}.Release|Any CPU.Build.0 = Release|Any CPU + {27A19D5D-9E2F-4B8A-9516-2FFD77B1052B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27A19D5D-9E2F-4B8A-9516-2FFD77B1052B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27A19D5D-9E2F-4B8A-9516-2FFD77B1052B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27A19D5D-9E2F-4B8A-9516-2FFD77B1052B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Redis/Lab.Redis.Client/src/Lab.Redis.Client/JsonSerializeFactory.cs b/Redis/Lab.Redis.Client/src/Lab.Redis.Client/JsonSerializeFactory.cs new file mode 100644 index 00000000..3d4365c5 --- /dev/null +++ b/Redis/Lab.Redis.Client/src/Lab.Redis.Client/JsonSerializeFactory.cs @@ -0,0 +1,21 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Unicode; + +namespace Lab.Redis.Client; + +public class JsonSerializeFactory +{ + public static JsonSerializerOptions CreateDefault() + { + return new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs), + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + } +} \ No newline at end of file diff --git a/Redis/Lab.Redis.Client/src/Lab.Redis.Client/Lab.Redis.Client.csproj b/Redis/Lab.Redis.Client/src/Lab.Redis.Client/Lab.Redis.Client.csproj new file mode 100644 index 00000000..6a137afd --- /dev/null +++ b/Redis/Lab.Redis.Client/src/Lab.Redis.Client/Lab.Redis.Client.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/Redis/Lab.Redis.Client/src/Lab.Redis.Client/RedisClient.cs b/Redis/Lab.Redis.Client/src/Lab.Redis.Client/RedisClient.cs new file mode 100644 index 00000000..90ad0deb --- /dev/null +++ b/Redis/Lab.Redis.Client/src/Lab.Redis.Client/RedisClient.cs @@ -0,0 +1,31 @@ +using StackExchange.Redis; + +namespace Lab.Redis.Client; + +public class RedisClient +{ + private static readonly Lazy s_connectionLazy; + private static string _setting; + + private ConnectionMultiplexer Instance => s_connectionLazy.Value; + + public IDatabase Database => this.Instance.GetDatabase(); + + static RedisClient() + { + s_connectionLazy = new Lazy(() => + { + if (string.IsNullOrWhiteSpace(_setting)) + { + return ConnectionMultiplexer.Connect("localhost"); + } + + return ConnectionMultiplexer.Connect(_setting); + }); + } + + public static void Init(string setting) + { + _setting = setting; + } +} \ No newline at end of file diff --git a/Redis/Lab.Redis.Client/src/Lab.Redis.Client/RedisConnection.cs b/Redis/Lab.Redis.Client/src/Lab.Redis.Client/RedisConnection.cs new file mode 100644 index 00000000..e03d47c8 --- /dev/null +++ b/Redis/Lab.Redis.Client/src/Lab.Redis.Client/RedisConnection.cs @@ -0,0 +1,27 @@ +using System.Collections.Concurrent; +using StackExchange.Redis; + +namespace Lab.Redis.Client; + +public class RedisConnection +{ + private static ConcurrentDictionary> s_connectionPool = new(); + + public IDatabase Connect(string setting = "localhost") + { + var connMultiplexer = s_connectionPool.GetOrAdd(setting, + new Lazy(() => ConnectionMultiplexer.Connect(setting))); + + return connMultiplexer.Value.GetDatabase(); + } + + public IDatabase GetDatabase(string setting = "localhost") + { + if (s_connectionPool.TryGetValue(setting, out var connMultiplexer)) + { + return connMultiplexer.Value.GetDatabase(); + } + + return default; + } +} \ No newline at end of file diff --git a/Redis/Lab.Redis.Client/src/Lab.Redis.Client/RedisDatabaseExtensions.cs b/Redis/Lab.Redis.Client/src/Lab.Redis.Client/RedisDatabaseExtensions.cs new file mode 100644 index 00000000..3c64bd50 --- /dev/null +++ b/Redis/Lab.Redis.Client/src/Lab.Redis.Client/RedisDatabaseExtensions.cs @@ -0,0 +1,41 @@ +using System.Text.Json; +using StackExchange.Redis; + +namespace Lab.Redis.Client; + +public static class RedisDatabaseExtensions +{ + public static bool IsExist(this IDatabase db, string key) + { + return db.KeyExists(key); + } + + public static void Set(this IDatabase db, string key, T value, + TimeSpan? expiry = default, + When when = When.Always, + CommandFlags flags = CommandFlags.None, + JsonSerializerOptions options = default) + { + db.StringSet(key, Serialize(value, options), expiry, when, flags); + } + + private static string Serialize(T value, JsonSerializerOptions options) + { + return JsonSerializer.Serialize(value, options); + } + + public static T Get(this IDatabase db, string key, JsonSerializerOptions options = default) + { + if (db.IsExist(key)) + { + return Deserialize(db.StringGet(key), options); + } + + return default; + } + + private static T? Deserialize(RedisValue value, JsonSerializerOptions options) + { + return JsonSerializer.Deserialize(value, options); + } +} \ No newline at end of file diff --git a/Redis/Lab.Redis.Client/src/TestProject1/RedisConnectionUnitTest.cs b/Redis/Lab.Redis.Client/src/TestProject1/RedisConnectionUnitTest.cs new file mode 100644 index 00000000..8701ba07 --- /dev/null +++ b/Redis/Lab.Redis.Client/src/TestProject1/RedisConnectionUnitTest.cs @@ -0,0 +1,35 @@ +using Lab.Redis.Client; +using StackExchange.Redis; + +namespace TestProject1; + +[TestClass] +public class RedisConnectionUnitTest +{ + [TestMethod] + public void SetDTO() + { + var connection = new RedisConnection(); + + // var database = connection.Connect("localhost:6379"); + var config = ConfigurationOptions.Parse("127.0.0.1:6379"); + var database = connection.Connect(config.ToString()); + var model = new Model + { + Name = "小章", + Age = 29 + }; + + database.Set("dto", model, options: JsonSerializeFactory.CreateDefault()); + var actual = database.Get("dto", options: JsonSerializeFactory.CreateDefault()); + Assert.AreEqual(model, actual); + } + + [Serializable] + record Model + { + public string Name { get; set; } + + public int Age { get; set; } + } +} \ No newline at end of file diff --git a/Redis/Lab.Redis.Client/src/TestProject1/TestProject1.csproj b/Redis/Lab.Redis.Client/src/TestProject1/TestProject1.csproj new file mode 100644 index 00000000..47d61bb8 --- /dev/null +++ b/Redis/Lab.Redis.Client/src/TestProject1/TestProject1.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + diff --git a/Redis/Lab.Redis.Client/src/TestProject1/UnitTest1.cs b/Redis/Lab.Redis.Client/src/TestProject1/UnitTest1.cs new file mode 100644 index 00000000..309d0339 --- /dev/null +++ b/Redis/Lab.Redis.Client/src/TestProject1/UnitTest1.cs @@ -0,0 +1,81 @@ +using Lab.Redis.Client; +using StackExchange.Redis; + +namespace TestProject1; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void StringSet() + { + RedisClient.Init("localhost"); + var db = new RedisClient().Database; + + // Set + string value = "Hello World"; + var key = "Test"; + db.StringSet(key, value); + + // set timeout 5min + db.StringSet(key, value, TimeSpan.FromSeconds(60)); + + // Get + var test = db.StringGet(key); + } + + [TestMethod] + public void Sets() + { + var db = new RedisClient().Database; + db.StringIncrement("visitCount"); + + // Set + string value = "Hello World"; + db.SetAdd("event", "001"); + db.SetAdd("event", "002"); + db.SetAdd("event", "003"); + var hashGetAll = db.HashGetAll("event"); + + // Get + var result = db.SetScan("event", "00*"); + result.ToList().ForEach(x => Console.WriteLine(x)); + + //然後是刪除的部份 + db.SetRemove("event", "002"); + } + + [TestMethod] + public void Hashset() + { + var db = new RedisClient().Database; + + db.HashSet("employee", new HashEntry[] + { + new("1", "anson"), + new("2", "kin"), + new("3", "jacky"), + }); + + //取出全部 + db.HashGetAll("employee").ToList().ForEach(x => Console.WriteLine(x)); + + //取出某筆 + db.HashGet("employee", 2); + + //刪除某筆 + db.HashDelete("employee", 2); + + //修改資料 + db.HashSet("employee", 3, "anson"); + } + + + [Serializable] + class MyClass + { + public string Name { get; set; } + + public int Age { get; set; } + } +} \ No newline at end of file diff --git a/Redis/Lab.Redis.Client/src/TestProject1/Usings.cs b/Redis/Lab.Redis.Client/src/TestProject1/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/Redis/Lab.Redis.Client/src/TestProject1/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.Test/DynamicCredentialTest.cs b/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.Test/DynamicCredentialTest.cs new file mode 100644 index 00000000..c1e7dcc2 --- /dev/null +++ b/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.Test/DynamicCredentialTest.cs @@ -0,0 +1,175 @@ +using System.Runtime.InteropServices; +using System.Security; +using System.Text.Json; +using VaultSharp; +using VaultSharp.Core; +using VaultSharp.V1.AuthMethods; +using VaultSharp.V1.AuthMethods.Token; +using VaultSharp.V1.Commons; +using VaultSharp.V1.SecretsEngines; +using VaultSharp.V1.SecretsEngines.Database; +using VaultSharp.V1.SecretsEngines.Database.Models.PostgreSQL; + +namespace Lab.HashiCorpVault.Test; + +[TestClass] +public class DynamicCredentialsTest +{ + private readonly string VaultToken = "你的驗證"; + private readonly string VaultServer = "http://127.0.0.1:8200"; + + [TestMethod] + public async Task _01_啟用資料庫() + { + var vaultClient = this.CreateVaultClient(); + + var enableSecretsEngine = new SecretsEngine + { + Type = new SecretsEngineType("database"), + Path = "database", + Description = "Database Secrets Engine" + }; + + await vaultClient.V1.System.MountSecretBackendAsync(enableSecretsEngine); + Console.WriteLine("Secrets 已成功寫入!"); + } + + [TestMethod] + public async Task _02_配置資料庫連線() + { + var vaultClient = this.CreateVaultClient(); + + // 寫入配置到 Vault + var config = new PostgreSQLConnectionConfigModel + { + PluginName = "postgresql-database-plugin", + AllowedRoles = new List { "my-db-role" }, + Username = "user", + Password = "password", + ConnectionUrl = "postgresql://{{username}}:{{password}}@localhost:5432/postgres?sslmode=disable", + + // 正確的 username_template 使用有效的模板函數 + UsernameTemplate = "{{uuid}}", + }; + var connectionName = "my-postgresql-database"; + + await vaultClient.V1.Secrets.Database.ConfigureConnectionAsync(connectionName, config); + + Console.WriteLine("已成功寫入 PostgreSQL 配置!"); + } + + [TestMethod] + public async Task _03_建立角色() + { + var vaultClient = this.CreateVaultClient(); + + // 定義創建語句 + var creationStatements = @" +CREATE ROLE ""{{name}}"" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO ""{{name}}""; +"; + var connectionName = "my-postgresql-database"; + + var role = new Role() + { + DatabaseProviderType = new DatabaseProviderType(connectionName), + DefaultTimeToLive = "1h", + MaximumTimeToLive = "24h", + CreationStatements = [creationStatements], + RevocationStatements = null, + RollbackStatements = null, + RenewStatements = null, + }; + var roleName = "my-db-role"; + await vaultClient.V1.Secrets.Database.CreateRoleAsync(roleName, role); + + Console.WriteLine("已成功寫入資料庫角色配置!"); + } + + [TestMethod] + public async Task _04_取得角色資訊() + { + var vaultClient = this.CreateVaultClient(); + + // 讀取資料庫角色的詳細資訊 + var roleName = "my-db-role"; + var roleInfo = await vaultClient.V1.Secrets.Database.ReadRoleAsync(roleName); + + // 輸出角色的詳細資訊 + + var roleJson = JsonSerializer.Serialize(roleInfo); + Console.WriteLine(roleJson); + } + + [TestMethod] + public async Task _05_建立憑證() + { + var vaultClient = this.CreateVaultClient(); + + // 讀取資料庫角色的憑證 + var roleName = "my-db-role"; + var credentials = await vaultClient.V1.Secrets.Database.GetCredentialsAsync(roleName); + + // 輸出角色的詳細資訊 + + var roleJson = JsonSerializer.Serialize(credentials); + Console.WriteLine(roleJson); + } + + [TestMethod] + public async Task _06_續約憑證() + { + var vaultClient = this.CreateVaultClient(); + + var leaseId = "database/creds/my-db-role/RhazfRMw84PiOJfFZD5QAEez"; + + // 續期租約 + var renewedLease = await vaultClient.V1.System.RenewLeaseAsync(leaseId, 3600); + Console.WriteLine("租約已成功續期!"); + var roleJson = JsonSerializer.Serialize(renewedLease); + Console.WriteLine(roleJson); + } + + [TestMethod] + public async Task _07_撤銷憑證() + { + var vaultClient = this.CreateVaultClient(); + + var leaseId = "database/creds/my-db-role/RhazfRMw84PiOJfFZD5QAEez"; + + // 續期租約 + await vaultClient.V1.System.RevokeLeaseAsync(leaseId); + Console.WriteLine("租約已成功撤銷!"); + } + + [TestMethod] + public async Task _08_取得所有憑證() + { + // 初始化 Vault Client + var vaultClient = this.CreateVaultClient(); + + // 指定要查詢的角色名稱 + var roleName = "my-db-role"; + + // var lease = await vaultClient.V1.System.GetLeaseAsync("database/creds/my-db-role/cpMIwcic5bwm0yTcI8UE5OUy"); + + try + { + var leases = await vaultClient.V1.System.GetAllLeasesAsync("database/creds/" + roleName); + var json = JsonSerializer.Serialize(leases); + Console.WriteLine(json); + } + catch (VaultApiException e) + { + Console.WriteLine(e.ToString()); + } + } + + private VaultClient CreateVaultClient() + { + var authMethod = new TokenAuthMethodInfo(this.VaultToken); + var vaultClientSettings = new VaultClientSettings(this.VaultServer, authMethod); + var vaultClient = new VaultClient(vaultClientSettings); + return vaultClient; + } +} \ No newline at end of file diff --git a/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.Test/Lab.HashiCorpVault.Test.csproj b/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.Test/Lab.HashiCorpVault.Test.csproj new file mode 100644 index 00000000..d45683d1 --- /dev/null +++ b/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.Test/Lab.HashiCorpVault.Test.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + false + true + Linux + + + + + + + + + + + + + + + diff --git a/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.Test/UnitTest1.cs b/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.Test/UnitTest1.cs new file mode 100644 index 00000000..9975d7b2 --- /dev/null +++ b/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.Test/UnitTest1.cs @@ -0,0 +1,226 @@ +using System.Runtime.InteropServices; +using System.Security; +using VaultSharp; +using VaultSharp.Core; +using VaultSharp.V1.AuthMethods.Token; +using VaultSharp.V1.SecretsEngines; + +namespace Lab.HashiCorpVault.Test; + +[TestClass] +public class UnitTest1 +{ + private readonly string VaultToken = "你的 token"; + private readonly string vaultServer = "http://127.0.0.1:8200"; + + [TestMethod] + public async Task 讀寫KV_V1() + { + // 初始化 Vault Client + var vaultClient = this.CreateVaultClient(); + + // 寫入 Secrets (等同於 vault kv put) + var secretData = new Dictionary + { + { "username", "admin" }, + { "password", "123456" } + }; + + var secretPath = "my-secret"; + var mountPath = "chechia-net/sre/workshop"; + + await vaultClient.V1.Secrets.KeyValue.V1.WriteSecretAsync(secretPath, secretData, mountPath); + Console.WriteLine("Secrets 已成功寫入!"); + + Console.WriteLine("讀取KV-V1:"); + var secret = await vaultClient.V1.Secrets.KeyValue.V1.ReadSecretAsync(secretPath, mountPath); + Console.WriteLine($"username={secret.Data["username"]}"); + Console.WriteLine($"password={secret.Data["password"]}"); + } + + [TestMethod] + public async Task 建立KV_V2() + { + // 初始化 Vault Client + var vaultClient = this.CreateVaultClient(); + + // 啟用 KV V2 存儲引擎 + var mountPath = "job/dream-team"; // 要啟用的路徑 + await CreateKvV2Path(vaultClient, mountPath); + Console.WriteLine($"KV V2 secrets engine enabled at path: {mountPath}"); + } + + [TestMethod] + public async Task 讀寫KV_V2() + { + // 初始化 Vault Client + var vaultClient = this.CreateVaultClient(); + + // 寫入 Secrets (等同於 vault kv put) + var secretData = new Dictionary + { + { "username", "admin" }, + { "password", "123456" } + }; + + var secretPath = "my-secret"; + var mountPath = "job/dream-team"; + + await vaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync(secretPath, secretData, mountPoint: mountPath); + Console.WriteLine("Secrets 已成功寫入!"); + + Console.WriteLine("讀取KV-V2:"); + var secret = await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(secretPath, mountPoint: mountPath); + Console.WriteLine($"username={secret.Data.Data["username"]}"); + Console.WriteLine($"password={secret.Data.Data["password"]}"); + } + + [TestMethod] + public async Task 停用KV_V2() + { + // 初始化 Vault Client + var vaultClient = this.CreateVaultClient(); + + var secretPath = "my-secret"; + var mountPath = "job/dream-team"; + await vaultClient.V1.System.UnmountSecretBackendAsync(mountPath); + + try + { + var secret = await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(secretPath, mountPoint: mountPath); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + } + + [TestMethod] + public async Task 讀寫KV_SecureString() + { + // 初始化 Vault Client + var vaultClient = this.CreateVaultClient(); + + // 寫入安全數據 + var secureData = new Dictionary + { + { "username", ConvertToSecureString("admin") }, + { "password", ConvertToSecureString("123456") } + }; + var secretPath = "my-secret"; + var mount = "chechia-net/sre/workshop"; + await WriteSecureSecretAsync(vaultClient, secretPath, mount, secureData); + Console.WriteLine("Secrets 已成功寫入!"); + + // 讀取安全數據 + var retrievedData = await ReadSecureSecretAsync(vaultClient, secretPath, mount); + + // 安全地使用數據 + using (retrievedData["username"]) + using (retrievedData["password"]) + { + // 在這裡使用安全字符串,避免將其轉換為普通字符串 + // 例如,可以將其傳遞給只接受 SecureString 的 API + retrievedData.TryGetValue("username", out var username); + } + } + + private static async Task WriteSecureSecretAsync(IVaultClient vaultClient, + string path, + string mountPoint, + Dictionary secureData) + { + var secretData = new Dictionary(); + + foreach (var kvp in secureData) + { + secretData[kvp.Key] = ConvertToUnsecureString(kvp.Value); + } + + await vaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync(path, secretData, mountPoint: mountPoint); + } + + private static async Task> ReadSecureSecretAsync(IVaultClient vaultClient, + string path, + string mountPoint) + { + var secret = await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(path, mountPoint: mountPoint); + var secureData = new Dictionary(); + + foreach (var kvp in secret.Data.Data) + { + secureData[kvp.Key] = ConvertToSecureString(kvp.Value.ToString()); + } + + return secureData; + } + + private static SecureString ConvertToSecureString(string plainText) + { + if (plainText == null) + throw new ArgumentNullException(nameof(plainText)); + + var secureString = new SecureString(); + foreach (char c in plainText) + { + secureString.AppendChar(c); + } + + secureString.MakeReadOnly(); + return secureString; + } + + private static string ConvertToUnsecureString(SecureString secureString) + { + if (secureString == null) + throw new ArgumentNullException(nameof(secureString)); + + IntPtr unmanagedString = IntPtr.Zero; + try + { + unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(secureString); + return Marshal.PtrToStringUni(unmanagedString); + } + finally + { + Marshal.ZeroFreeGlobalAllocUnicode(unmanagedString); + } + } + + private VaultClient CreateVaultClient() + { + var authMethod = new TokenAuthMethodInfo(this.VaultToken); + var vaultClientSettings = new VaultClientSettings(this.vaultServer, authMethod); + var vaultClient = new VaultClient(vaultClientSettings); + return vaultClient; + } + + private static async Task CreateKvV2Path(IVaultClient vaultClient, string path) + { + try + { + await vaultClient.V1.System.MountSecretBackendAsync(new SecretsEngine + { + Path = path, + Type = new SecretsEngineType("kv-v2") + }); + + Console.WriteLine($"Successfully created kv-v2 path: {path}"); + } + catch (VaultApiException ex) + { + if (ex.Message.Contains("path is already in use")) + { + Console.WriteLine($"The path '{path}' is already in use. It may already be mounted."); + } + else + { + Console.WriteLine($"Error creating kv-v2 path: {ex.Message}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Unexpected error: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.sln b/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.sln new file mode 100644 index 00000000..3713faca --- /dev/null +++ b/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.sln @@ -0,0 +1,21 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.HashiCorpVault.Test", "Lab.HashiCorpVault.Test\Lab.HashiCorpVault.Test.csproj", "{4462F29F-5107-4821-89CB-8DF155F795FC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4742DBF2-719D-4C80-923E-4F5308538771}" + 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 + {4462F29F-5107-4821-89CB-8DF155F795FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4462F29F-5107-4821-89CB-8DF155F795FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4462F29F-5107-4821-89CB-8DF155F795FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4462F29F-5107-4821-89CB-8DF155F795FC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Secrets Manager/Lab.HashiCorpVault/docker-compose.yml b/Secrets Manager/Lab.HashiCorpVault/docker-compose.yml new file mode 100644 index 00000000..2bbd7ae5 --- /dev/null +++ b/Secrets Manager/Lab.HashiCorpVault/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.7' +services: + hashicorp: + image: hashicorp/vault:latest + container_name: hashicorp + ports: + - "8200:8200" + environment: + - VAULT_DEV_ROOT_TOKEN_ID=myroot + - VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200 + cap_add: + - IPC_LOCK \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/.gitlab-ci.yml b/Shnapshot/Lab.Snapshot/.gitlab-ci.yml new file mode 100644 index 00000000..a269c045 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/.gitlab-ci.yml @@ -0,0 +1,18 @@ + stages: + - build + - test + + job1: + stage: build + script: + - echo "This job runs in the build stage." + + last-job: + stage: .post + script: + - echo "This job runs in the .post stage, after all other stages." + + job2: + stage: test + script: + - echo "This job runs in the test stage." \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/Account.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/Account.cs new file mode 100644 index 00000000..791c4887 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/Account.cs @@ -0,0 +1,8 @@ +namespace Lab.Snapshot.DB; + +public record Account +{ + public string Id { get; set; } + + public string Type { get; set; } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/Lab.Snapshot.DB.csproj b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/Lab.Snapshot.DB.csproj new file mode 100644 index 00000000..987967e8 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/Lab.Snapshot.DB.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/MemberDataEntity.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/MemberDataEntity.cs new file mode 100644 index 00000000..5c48f427 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/MemberDataEntity.cs @@ -0,0 +1,35 @@ +namespace Lab.Snapshot.DB; + +public record MemberDataEntity +{ + public string Id { get; set; } + + public Profile Profile { get; set; } + + public List Accounts { 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; } + + public Dictionary ToDictionary() + { + return new Dictionary + { + { nameof(this.Id), this.Id }, + { nameof(this.Profile), this.Profile }, + { nameof(this.Accounts), this.Accounts }, + { nameof(this.CreatedAt), this.CreatedAt }, + { nameof(this.CreatedBy), this.CreatedBy }, + { nameof(this.UpdatedAt), this.UpdatedAt }, + { nameof(this.UpdatedBy), this.UpdatedBy }, + { nameof(this.Version), this.Version } + }; + } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/MemberDbContext.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/MemberDbContext.cs new file mode 100644 index 00000000..89ead280 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/MemberDbContext.cs @@ -0,0 +1,104 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.Extensions.Logging; + +namespace Lab.Snapshot.DB; + +public class MemberDbContext : DbContext +{ + public DbSet Members { get; set; } + + public DbSet Snapshots { get; set; } + + private static readonly bool[] s_migrated = { false }; + + public MemberDbContext(DbContextOptions options) + : base(options) + { + if (!s_migrated[0]) + { + lock (s_migrated) + { + if (!s_migrated[0]) + { + var migrations = this.Database.GetMigrations(); + if (migrations.Any()) + { + this.Database.Migrate(); + } + else + { + this.Database.EnsureCreated(); + } + + s_migrated[0] = true; + } + } + } + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // optionsBuilder.UseExceptionProcessor(); + optionsBuilder.ConfigureWarnings(b => + b.Log((CoreEventId.SaveChangesFailed, LogLevel.Warning), + (RelationalEventId.CommandError, LogLevel.Warning))); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.HasSequence("member_collection_seqno") + .StartsAt(1) + .IncrementsBy(1); + + modelBuilder.ApplyConfiguration(new MemberConfiguration()); + modelBuilder.ApplyConfiguration(new SnapshotConfiguration()); + } + + internal class MemberConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Member"); + builder.HasKey(p => new + { + p.Id + }); + builder.Property(p => p.Accounts).HasColumnType("jsonb").IsRequired(); + builder.Property(p => p.Profile).HasColumnType("jsonb").IsRequired(false); + builder.Property(p => p.CreatedAt).HasColumnType("timestamp with time zone").IsRequired(); + builder.Property(p => p.CreatedBy).HasMaxLength(50).IsRequired(); + builder.Property(x => x.UpdatedAt).HasColumnType("timestamp with time zone").IsRequired(); + builder.Property(x => x.UpdatedBy).HasMaxLength(50).IsRequired(); + builder.Property(p => p.Version).IsRequired(); + + // indexes + builder.HasIndex(x => x.Accounts).HasMethod("GIN"); + } + } + + internal class SnapshotConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Snapshot"); + builder.HasKey(x => new + { + x.Id, + x.Version + }); + builder.Property(x => x.Data).HasColumnType("jsonb").IsRequired(); + builder.Property(x => x.DataFormat).IsRequired(); + builder.Property(x => x.DataType).IsRequired(); + builder.Property(x => x.CreatedAt).HasColumnType("timestamp with time zone").IsRequired(); + builder.Property(x => x.CreatedBy).HasMaxLength(50).IsRequired(); + builder.Property(p => p.Version).IsRequired(); + + // indexes + builder.HasIndex(x => x.Data).HasMethod("GIN"); + } + } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/MemberDbContextFactory.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/MemberDbContextFactory.cs new file mode 100644 index 00000000..10d0d166 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/MemberDbContextFactory.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Lab.Snapshot.DB; + +public class MemberDbContextFactory : IDesignTimeDbContextFactory +{ + public MemberDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql("Host=localhost;Database=member;Username=postgres;Password=guest"); + + return new MemberDbContext(optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/Profile.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/Profile.cs new file mode 100644 index 00000000..36f3c0b9 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/Profile.cs @@ -0,0 +1,8 @@ +namespace Lab.Snapshot.DB; + +public record Profile +{ + public int Age { get; set; } + + public string Name { get; set; } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/SnapshotDataEntity.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/SnapshotDataEntity.cs new file mode 100644 index 00000000..76a263ca --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.DB/SnapshotDataEntity.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Lab.Snapshot.DB; + +public class SnapshotDataEntity +{ + public string Id { get; set; } + + public string DataType { get; set; } + + [Column(TypeName = "jsonb")] + public JsonNode Data { get; set; } + + public string DataFormat { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public string CreatedBy { get; set; } + + public int Version { get; set; } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/Lab.Snapshot.Test.csproj b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/Lab.Snapshot.Test.csproj new file mode 100644 index 00000000..9fd06f7c --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/Lab.Snapshot.Test.csproj @@ -0,0 +1,37 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/NpgsqlGenerateScript.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/NpgsqlGenerateScript.cs new file mode 100644 index 00000000..21bc4797 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/NpgsqlGenerateScript.cs @@ -0,0 +1,28 @@ +namespace Lab.Snapshot.Test; + +internal class NpgsqlGenerateScript +{ + public static string ClearAllRecord() + { + return @" +DO $$ +DECLARE row RECORD; +BEGIN + FOR row IN SELECT table_name + FROM information_schema.tables + WHERE table_type='BASE TABLE' + AND table_schema='public' + AND table_name NOT IN ('admins', 'admin_roles', '__EFMigrationsHistory') + LOOP + EXECUTE format('TRUNCATE TABLE %I CONTINUE IDENTITY CASCADE;', row.table_name); + END LOOP; +END; +$$; +"; + } + + public static string ReseedMemberCollectionSeq() + { + return "ALTER SEQUENCE member_collection_seqno RESTART WITH 1;"; + } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/ServiceConfiguration.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/ServiceConfiguration.cs new file mode 100644 index 00000000..0733b02d --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/ServiceConfiguration.cs @@ -0,0 +1,25 @@ +using Lab.Snapshot.DB; +using Lab.Snapshot.WebAPI; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.Snapshot.Test; + +public class ServiceConfiguration +{ + public static void ConfigDb(IServiceCollection services) + { + services.AddSingleton(p => { return LoggerFactory.Create(builder => { builder.AddConsole(); }); }); + services.AddDbContextFactory((p, options) => + { + var connectionString = Environment.GetEnvironmentVariable(EnvironmentNames.DbConnectionString); + options.UseNpgsql(connectionString, + builder => builder.EnableRetryOnFailure( + 10, + TimeSpan.FromSeconds(30), + new List { "57P01" })) + ; + }); + } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/TestAssistant.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/TestAssistant.cs new file mode 100644 index 00000000..24aeb592 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/TestAssistant.cs @@ -0,0 +1,13 @@ +using Npgsql; + +namespace Lab.Snapshot.Test; + +public class TestAssistant +{ + public const string DbConnectionString = + "Host=localhost;Port=5432;Database=member_integration_test;Username=postgres;Password=guest"; + + public static DateTimeOffset Now { get; set; } = DateTimeOffset.UtcNow; + + public static string UserId { get; set; } = "@@TestUser@@"; +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/TestHook.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/TestHook.cs new file mode 100644 index 00000000..2a49f66b --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/TestHook.cs @@ -0,0 +1,38 @@ +using Lab.Snapshot.DB; +using Lab.Snapshot.WebAPI; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Lab.Snapshot.Test; + +[TestClass] +public class TestHook +{ + private static readonly IServiceCollection s_services = new ServiceCollection(); + static IServiceProvider s_serviceProvider; + + [AssemblyInitialize] + public static void AssemblyInitialize(TestContext context) + { + Console.WriteLine("AssemblyInitialize"); + Environment.SetEnvironmentVariable(EnvironmentNames.DbConnectionString, TestAssistant.DbConnectionString); + ServiceConfiguration.ConfigDb(s_services); + s_serviceProvider = s_services.BuildServiceProvider(); + using var dbContext = s_serviceProvider.GetService>().CreateDbContext(); + + // drop and create database + dbContext.Database.EnsureDeleted(); + dbContext.Database.EnsureCreated(); + } + + [AssemblyCleanup] + public static void AssemblyCleanup() + { + Console.WriteLine("AssemblyCleanup"); + + // drop database + using var dbContext = s_serviceProvider.GetService>().CreateDbContext(); + + // dbContext.Database.EnsureDeleted(); + } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/UnitTest1.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/UnitTest1.cs new file mode 100644 index 00000000..514e1f03 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/UnitTest1.cs @@ -0,0 +1,702 @@ +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.JsonDiffPatch; +using System.Text.Json.JsonDiffPatch.Diffs.Formatters; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using FluentAssertions; +using JsonDiffPatchDotNet; +using JsonDiffPatchDotNet.Formatters.JsonPatch; +using Lab.Snapshot.DB; +using Lab.Snapshot.WebAPI; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using JsonConverter = System.Text.Json.Serialization.JsonConverter; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace Lab.Snapshot.Test; + +[TestClass] +public class UnitTest1 +{ + private IServiceProvider _serviceProvider; + + IDbContextFactory DbContextFactory => + _serviceProvider.GetService>(); + + static JsonSerializerOptions JsonSerializerOptions = new() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = + { + new JsonStringEnumConverter() + } + }; + + [TestInitialize] + public void TestInitialize() + { + Console.WriteLine("TestInitialize"); + if (this._serviceProvider == null) + { + var services = new ServiceCollection(); + ServiceConfiguration.ConfigDb(services); + this._serviceProvider = services.BuildServiceProvider(); + } + + this.CleanAllRecord(this.DbContextFactory.CreateDbContext()); + } + + [TestCleanup] + public void TestCleanup() + { + Console.WriteLine("TestCleanup"); + this.CleanAllRecord(this.DbContextFactory.CreateDbContext()); + } + + /// + /// 刪除資料庫所有的資料 + /// + /// + private void CleanAllRecord(DbContext dbContext) + { + dbContext.Database.ExecuteSqlRaw(NpgsqlGenerateScript.ClearAllRecord()); + } + + [TestMethod] + public async Task Diff() + { + var left = """ + { + "id": 100, + "revision": 5, + "items": [ + "car", + "bus" + ], + "tagline": "I can't do it. This text is too long for me to handle! Please help me JsonDiffPatch!", + "author": "wbish" + } + """; + + var right = """ + { + "id": 100, + "revision": 6, + "items": [ + "bike", + "bus", + "car" + ], + "tagline": "I can do it. This text is not too long. Thanks JsonDiffPatch!", + "author": { + "first": "w", + "last": "bish" + } + } + """; + + var jdp = new JsonDiffPatch(); + var output = jdp.Diff(left, right); + var formatter = new JsonDeltaFormatter(); + var operations = formatter.Format(output); + var patch = new JsonDiffPatch().Diff(left, right); + } + + [TestMethod] + public async Task Patch() + { + var left = JObject.Parse("{ \"name\": \"Justin\" }"); + var right = JObject.Parse("{ \"name\" : \"John\", \"age\": 34 }"); + var patch = new JsonDiffPatch().Diff(left, right); + var formatter = new JsonDeltaFormatter(); + var operations = formatter.Format(patch); + var jToken = new JsonDiffPatch().Patch(left, patch); + } + + [TestMethod] + public async Task Newtonsoft_DiffPatch() + { + var oldMember = new MemberDataEntity + { + Id = "1", + Profile = new Profile + { + Age = 19, + Name = "yao-chang" + }, + Accounts = new List + { + new() + { + Id = "yao", + Type = "VIP" + } + }, + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "test", + UpdatedAt = DateTimeOffset.UtcNow, + UpdatedBy = "test", + Version = 1 + }; + + var newMember = new MemberDataEntity + { + Id = "1", + Profile = new Profile + { + Age = 19, + Name = "小章" + }, + Accounts = new List + { + new() + { + Id = "yao", + Type = "VIP" + }, + new() + { + Id = "yao1", + Type = "VIP1" + } + }, + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "test", + UpdatedAt = DateTimeOffset.UtcNow, + UpdatedBy = "test", + Version = 2 + }; + + var oldData = JsonConvert.SerializeObject(oldMember); + var newData = JsonConvert.SerializeObject(newMember); + var diff = new JsonDiffPatch().Diff(oldData, newData); + var patchData = new JsonDiffPatch().Patch(oldData, diff); + + var actual = JsonConvert.DeserializeObject(patchData); + actual.Should().BeEquivalentTo(newMember); + } + + [TestMethod] + public async Task SystemTextJson_DiffPatch() + { + var oldMember = new MemberDataEntity + { + Id = "1", + Profile = new Profile + { + Age = 19, + Name = "yao-chang" + }, + Accounts = new List + { + new() + { + Id = "yao", + Type = "VIP" + } + }, + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "test", + UpdatedAt = DateTimeOffset.UtcNow, + UpdatedBy = "test", + Version = 1 + }; + + var newMember = new MemberDataEntity + { + Id = "1", + Profile = new Profile + { + Age = 19, + Name = "小章" + }, + Accounts = new List + { + new() + { + Id = "yao", + Type = "VIP" + }, + new() + { + Id = "yao1", + Type = "VIP1" + } + }, + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "test", + UpdatedAt = DateTimeOffset.UtcNow, + UpdatedBy = "test", + Version = 2 + }; + + var oldData = JsonSerializer.Serialize(oldMember, JsonSerializerOptions); + var newData = JsonSerializer.Serialize(newMember, JsonSerializerOptions); + + var diff = JsonDiffPatcher.Diff(oldData, newData, new JsonDiffOptions + { + JsonElementComparison = JsonElementComparison.Semantic + }); + var diff1 = JsonDiffPatcher.Diff(oldData, newData); + var diff2 = JsonDiffPatcher.Diff(oldData, newData, new JsonPatchDeltaFormatter(), new JsonDiffOptions + { + JsonElementComparison = JsonElementComparison.Semantic + }); + var result = JsonNode.Parse(oldData); + JsonDiffPatcher.Patch(ref result, diff); + var actual = result.Deserialize(JsonSerializerOptions); + actual.Should().BeEquivalentTo(newMember); + } + + /// + /// 集合內容一樣,但順序不一樣 + /// + [TestMethod] + public async Task SystemTextJson_Diff_ListSortDiff() + { + var first = GenerateMember(20, "jordan", new Account + { + Id = "jordan1", + Type = "VVVIP" + }, 1); + first.Accounts = first.Accounts.OrderBy(p => p.Id).ToList(); + var second = GenerateMember(20, "jordan", new Account + { + Id = "jordan1", + Type = "VVVIP" + }, 1); + second.Accounts = second.Accounts.OrderByDescending(p => p.Id).ToList(); + var options = new JsonDiffOptions + { + JsonElementComparison = JsonElementComparison.Semantic, + }; + var jsonPatchDeltaFormatter = new JsonPatchDeltaFormatter(); + var left = JsonSerializer.Serialize(first.Accounts, JsonSerializerOptions); + var right = JsonSerializer.Serialize(second.Accounts, JsonSerializerOptions); + + var diff = JsonDiffPatcher.Diff(left + , right + + // , jsonPatchDeltaFormatter + , options + ); + + first.Accounts.Should().BeEquivalentTo(second.Accounts); + + var result = JsonNode.Parse(left); + + JsonDiffPatcher.Patch(ref result, diff); + var actual = result.Deserialize>(JsonSerializerOptions); + actual.Should().BeEquivalentTo(second.Accounts); + + result = JsonNode.Parse(right); + JsonDiffPatcher.Patch(ref result, diff); + actual = result.Deserialize>(JsonSerializerOptions); + actual.Should().BeEquivalentTo(second.Accounts); + } + + /// + /// 集合內容一樣,順序一樣 + /// + [TestMethod] + public async Task SystemTextJson_Diff_ListSortSame() + { + var first = GenerateMember(20, "jordan", new Account + { + Id = "jordan1", + Type = "VVVIP" + }, 1); + first.Accounts = first.Accounts.OrderBy(p => p.Id).ToList(); + var left = JsonSerializer.Serialize(first.Accounts, JsonSerializerOptions); + var right = JsonSerializer.Serialize(first.Accounts, JsonSerializerOptions); + var options = new JsonDiffOptions + { + JsonElementComparison = JsonElementComparison.Semantic + }; + var jsonPatchDeltaFormatter = new JsonPatchDeltaFormatter(); + var diff = JsonDiffPatcher.Diff(left + , right + , jsonPatchDeltaFormatter + , options + ); + diff.AsArray().Count.Should().Be(0); + } + + [TestMethod] + public async Task SystemTextJson_RestoreInMemory() + { + var oldMember = new MemberDataEntity + { + Id = "1", + Profile = new Profile + { + Age = 19, + Name = "小章" + }, + Accounts = new List + { + new() + { + Id = "yao", + Type = "VIP" + }, + new() + { + Id = "yao1", + Type = "VIP1" + } + }, + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "test", + UpdatedAt = DateTimeOffset.UtcNow, + UpdatedBy = "test", + Version = 2 + }; + + var newMember = new MemberDataEntity + { + Id = "1", + Profile = new Profile + { + Age = 19, + Name = "小章" + }, + Accounts = new List + { + new() + { + Id = "yao1", + Type = "VIP1" + }, + new() + { + Id = "yao", + Type = "VIP" + }, + }, + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "test", + UpdatedAt = DateTimeOffset.UtcNow, + UpdatedBy = "test", + Version = 2 + }; + + var oldData = JsonSerializer.Serialize(oldMember, JsonSerializerOptions); + var newData = JsonSerializer.Serialize(newMember, JsonSerializerOptions); + + var diff = JsonDiffPatcher.Diff(oldData, newData, new JsonDiffOptions + { + JsonElementComparison = JsonElementComparison.Semantic + }); + var diff1 = JsonDiffPatcher.Diff(oldData, newData); + var diff2 = JsonDiffPatcher.Diff(oldData, newData, new JsonPatchDeltaFormatter(), new JsonDiffOptions + { + JsonElementComparison = JsonElementComparison.Semantic + }); + var result = JsonNode.Parse(oldData); + JsonDiffPatcher.Patch(ref result, diff); + var actual = result.Deserialize(JsonSerializerOptions); + actual.Should().BeEquivalentTo(newMember); + } + + [TestMethod] + public async Task SystemTextJson_RestoreInDb() + { + await this.InsertOrGetAsync(1); + await this.Update(GenerateMember(20, "jordan", new Account + { + Id = "jordan1", + Type = "VVVIP" + }, 2)); + await this.Update(GenerateMember(18, "yao", null, 3)); + await this.Update(GenerateMember(30, "yao1", new Account + { + Id = "chang", + Type = "Normal" + }, 4)); + await this.Update(GenerateMember(32, "yao-chang", null, 5)); + await this.Update(GenerateMember(32, "yao-chang", null, 5)); + + await using var dbContext = await this.DbContextFactory.CreateDbContextAsync(); + var snapshots = await dbContext.Snapshots + .Where(p => p.Id == "1") + .OrderBy(p => p.Version) + .AsNoTracking() + .ToListAsync(); + + JsonNode full = null; + foreach (var snapshot in snapshots) + { + if (snapshot.Version == 1) + { + full = snapshot.Data; + continue; + } + + JsonDiffPatcher.Patch(ref full, snapshot.Data); + } + + var actual = full.Deserialize(JsonSerializerOptions); + var expected = await dbContext.Members.FirstOrDefaultAsync(p => p.Id == "1"); + actual.Should().BeEquivalentTo(expected); + } + + private async Task Update(MemberDataEntity newMember) + { + var now = TestAssistant.Now; + await using var dbContext = await this.DbContextFactory.CreateDbContextAsync(); + + // 取得資料 sub query + var queryable = from member in dbContext.Members + where member.Id == "1" + select new + { + member, + snapshots = dbContext.Snapshots + .Where(p => p.Id == "1") + .ToList() + }; + var data = await queryable.FirstOrDefaultAsync(); + var oldMember = data.member; + newMember.Version = oldMember.Version; + + // 比對差異 + var oldData = JsonSerializer.Serialize(oldMember, JsonSerializerOptions); + var newData = JsonSerializer.Serialize(newMember, JsonSerializerOptions); + var diff = JsonDiffPatcher.Diff(oldData, newData, + + // new JsonPatchDeltaFormatter(), + new JsonDiffOptions + { + JsonElementComparison = JsonElementComparison.Semantic, + }); + if (diff == null) + { + return; + } + + // 有異動,進版號 + newMember.Version = oldMember.Version + 1; + + dbContext.Entry(oldMember).CurrentValues.SetValues(newMember); + var entity = new SnapshotDataEntity + { + Id = oldMember.Id, + Data = diff, + DataType = typeof(MemberDataEntity).ToString(), + DataFormat = DataFormat.Diff.ToString(), + CreatedAt = now, + CreatedBy = "test", + Version = newMember.Version + }; + dbContext.Snapshots.Add(entity); + + var changes = await dbContext.SaveChangesAsync(); + } + + private static MemberDataEntity GenerateMember(int age, string name, Account account, int version) + { + var now = TestAssistant.Now; + var newMember = new MemberDataEntity() + { + Id = "1", + Profile = new Profile + { + Age = age, + Name = name + }, + Accounts = new List + { + new() + { + Id = "yao", + Type = "VIP" + }, + new() + { + Id = "yao1", + Type = "VIP1" + } + }, + CreatedAt = now, + CreatedBy = "test", + UpdatedAt = now, + UpdatedBy = "test", + }; + if (account != null) + { + newMember.Accounts.Add(account); + } + + return newMember; + } + + [TestMethod] + public async Task 更新資料產生差異快照() + { + var oldMember = await this.InsertOrGetAsync(1); + var newMember = new MemberDataEntity() + { + Id = "1", + Profile = new Profile + { + Age = 19, + Name = "小章" + }, + Accounts = new List + { + new() + { + Id = "yao", + Type = "VIP" + }, + new() + { + Id = "yao1", + Type = "VIP1" + } + }, + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "test", + UpdatedAt = DateTimeOffset.UtcNow, + UpdatedBy = "test", + Version = 2 + }; + + var search = "[{\"Id\": \"yao\"}]"; + + await using var dbContext = await this.DbContextFactory.CreateDbContextAsync(); + + var queryable = from member in dbContext.Members + join snapshot in dbContext.Snapshots + on + new + { + member.Id, + member.Version + } + equals + new + { + snapshot.Id, + snapshot.Version + } + where member.Id == "1" + select new { member, snapshot }; + + var data = await queryable.FirstOrDefaultAsync(); + var oldData = JsonConvert.SerializeObject(oldMember); + var newData = JsonConvert.SerializeObject(newMember); + var jsonDiffPatch = new JsonDiffPatch(); + var diff = jsonDiffPatch.Diff(oldData, newData); + var formatter = new JsonDeltaFormatter(); + + dbContext.Entry(data.member).CurrentValues.SetValues(newData); + dbContext.Snapshots.Add(new SnapshotDataEntity + { + Id = oldMember.Id, + Data = JsonNode.Parse(diff), + DataType = typeof(MemberDataEntity).ToString(), + DataFormat = "Diff", + CreatedAt = data.snapshot.CreatedAt, + CreatedBy = data.snapshot.CreatedBy, + Version = newMember.Version + }); + var changes = dbContext.SaveChanges(); + } + + [TestMethod] + public async Task ORM查詢Jsonb() + { + await this.InsertOrGetAsync(1); + + var now = TestAssistant.Now; + var userId = TestAssistant.UserId; + var search = "[{\"Id\": \"yao\"}]"; + + await using var dbContext = await this.DbContextFactory.CreateDbContextAsync(); + + // var member = dbContext.Members + // .Where(s => EF.Functions.JsonContains(s.Accounts, search)) + // .AsNoTracking() + // .FirstOrDefault(); + + var queryable = from member in dbContext.Members + join snapshot in dbContext.Snapshots + on + new + { + member.Id, + member.Version + } + equals + new + { + snapshot.Id, + snapshot.Version + } + where EF.Functions.JsonContains(member.Accounts, search) + select new { member, snapshot }; + var data = await queryable.AsNoTracking().FirstOrDefaultAsync(); + data.member.Id.Should().Be("1"); + } + + private async Task InsertOrGetAsync(int version) + { + await using var dbContext = await this.DbContextFactory.CreateDbContextAsync(); + + var now = TestAssistant.Now; + var userId = TestAssistant.UserId; + var member = await dbContext.Members.Where(p => p.Id == "1").AsNoTracking().FirstOrDefaultAsync(); + if (member != null) + { + return member; + } + + member = new MemberDataEntity + { + Id = "1", + Profile = new Profile + { + Age = 18, + Name = "yao-chang" + }, + Accounts = new List() + { + new() + { + Id = "yao", + Type = "VIP" + } + }, + CreatedAt = now, + CreatedBy = userId, + UpdatedAt = now, + UpdatedBy = userId, + Version = version + }; + dbContext.Members.Add(member); + + dbContext.Snapshots.Add(new SnapshotDataEntity + { + Id = member.Id, + Data = JsonNode.Parse(JsonSerializer.Serialize(member, JsonSerializerOptions)), + DataType = typeof(MemberDataEntity).ToString(), + DataFormat = "Full", + CreatedAt = now, + CreatedBy = userId, + Version = member.Version + }); + var count = await dbContext.SaveChangesAsync(); + return member; + } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/Usings.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.Test/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ContextAccessor.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ContextAccessor.cs new file mode 100644 index 00000000..d3f23e9c --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ContextAccessor.cs @@ -0,0 +1,34 @@ +namespace Lab.Snapshot.WebAPI; + +public record AuthContext +{ + public string TraceId { get; set; } + + public DateTimeOffset Now { get; set; } + + public string UserId { get; set; } +} + +public interface IContextGetter where T : class +{ + T? Get(); +} + +public interface IContextSetter where T : class +{ + void Set(T value); +} + +public class ContextAccessor : IContextGetter, IContextSetter where T : class +{ + private static AsyncLocal> _context = new(); + + public T? Get() => _context.Value?.Context; + + public void Set(T value) => _context.Value = new ContextHolder { Context = value }; +} + +public class ContextHolder where T : class +{ + public T Context { get; set; } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Controllers/MembersController.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Controllers/MembersController.cs new file mode 100644 index 00000000..56ec39d2 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Controllers/MembersController.cs @@ -0,0 +1,95 @@ +using System.Net; +using Lab.Snapshot.WebAPI.ServiceModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Formatters; + +namespace Lab.Snapshot.WebAPI.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class MembersController : ControllerBase +{ + private readonly ILogger _logger; + readonly MemberRepository _memberRepository; + + public MembersController(ILogger logger, MemberRepository memberRepository) + { + this._logger = logger; + this._memberRepository = memberRepository; + } + + [HttpPost(Name = "InsertMember")] + public async Task> Post(InsertMemberRequest request) + { + var insertMemberResult = await this._memberRepository.InsertMemberAsync(request); + if (insertMemberResult.Failure != null) + { + return new ObjectResult(insertMemberResult.Failure) + { + ContentTypes = new MediaTypeCollection() + { + "application/json" + }, + StatusCode = (int)HttpStatusCode.BadRequest + }; + } + + return this.NoContent(); + } + + [HttpPut("{currentAccount}/bind", Name = "BindMember")] + public async Task> Bind(string currentAccount, UpdateMemberRequest request) + { + var bindMemberResult = await this._memberRepository.BindMemberAsync(currentAccount, request); + if (bindMemberResult.Failure != null) + { + return new ObjectResult(bindMemberResult.Failure) + { + ContentTypes = new MediaTypeCollection() + { + "application/json" + }, + StatusCode = (int)HttpStatusCode.BadRequest + }; + } + + return this.NoContent(); + } + + [HttpGet("{account}:query-account-id", Name = "QueryMemberByAccount")] + public async Task> QueryMemberByAccountAsync(string account, int? version) + { + var queryMemberResult = await this._memberRepository.QueryMemberByAccountAsync(account, version); + if (queryMemberResult == null) + { + return this.NotFound(); + } + + return this.Ok(queryMemberResult); + } + + [HttpGet(Name = "GetMembers")] + //todo:待實作分頁 + public async Task> GetMembersAsync() + { + var getMemberResult = await this._memberRepository.GetMembersAsync(); + if (getMemberResult == null) + { + return this.NotFound(); + } + + return this.Ok(getMemberResult); + } + + [HttpGet("{id}", Name = "GetMember")] + public async Task> GetMemberAsync(string id, int? version) + { + var getMemberResult = await this._memberRepository.GetMemberAsync(id, version); + if (getMemberResult == null) + { + return this.NotFound(); + } + + return this.Ok(getMemberResult); + } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/DataFormat.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/DataFormat.cs new file mode 100644 index 00000000..f28f6841 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/DataFormat.cs @@ -0,0 +1,7 @@ +namespace Lab.Snapshot.WebAPI; + +public enum DataFormat +{ + Full, + Diff +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/EnvironmentNames.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/EnvironmentNames.cs new file mode 100644 index 00000000..68b5b37c --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/EnvironmentNames.cs @@ -0,0 +1,16 @@ +namespace Lab.Snapshot.WebAPI; + +public class EnvironmentNames +{ + public const string DbConnectionString = "DB_CONNECTION_STRING"; + + public static Dictionary Values = new() + { + { + DbConnectionString, + "Host=localhost;Port=5432;Database=member_integration_test;Username=postgres;Password=guest" + } + }; + + +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Failure.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Failure.cs new file mode 100644 index 00000000..f8b25991 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Failure.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace Lab.Snapshot.WebAPI; + +public enum FailureCode +{ + MemberExist, + MemberNotExist +} + +public class Failure +{ + public FailureCode Code { get; set; } + + public string Message { get; set; } + + public Failure(FailureCode code, string message) + { + this.Code = code; + this.Message = message; + } + + public List Failures { get; set; } + + [JsonIgnore] + public Exception Fetal { get; set; } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ISystemClock.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ISystemClock.cs new file mode 100644 index 00000000..563abfe1 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ISystemClock.cs @@ -0,0 +1,6 @@ +namespace Lab.Snapshot.WebAPI; + +public interface ISystemClock +{ + DateTimeOffset Now { get; } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/JsonSerializeFactory.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/JsonSerializeFactory.cs new file mode 100644 index 00000000..72045e37 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/JsonSerializeFactory.cs @@ -0,0 +1,21 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lab.Snapshot.WebAPI; + +internal class JsonSerializeFactory +{ + public static JsonSerializerOptions Apply(JsonSerializerOptions options) + { + options.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + options.PropertyNameCaseInsensitive = true; + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.Converters.Add(new JsonStringEnumConverter()); + return options; + } + + public static JsonSerializerOptions Init() => Apply(new JsonSerializerOptions()); +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Lab.Snapshot.WebAPI.csproj b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Lab.Snapshot.WebAPI.csproj new file mode 100644 index 00000000..5cece286 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Lab.Snapshot.WebAPI.csproj @@ -0,0 +1,25 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Mapper.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Mapper.cs new file mode 100644 index 00000000..386830b9 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Mapper.cs @@ -0,0 +1,17 @@ +namespace Lab.Snapshot.WebAPI; + +public class Mapper +{ + public class DomainToResponseMappingProfile : AutoMapper.Profile + { + public DomainToResponseMappingProfile() + { + this.CreateMap(); + + this.CreateMap(); + this.CreateMap(); + this.CreateMap(); + this.CreateMap(); + } + } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/MemberRepository.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/MemberRepository.cs new file mode 100644 index 00000000..39bbd1e3 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/MemberRepository.cs @@ -0,0 +1,295 @@ +using System.Text.Json; +using System.Text.Json.JsonDiffPatch; +using System.Text.Json.Nodes; +using AutoMapper; +using Lab.Snapshot.DB; +using Lab.Snapshot.WebAPI.ServiceModels; +using Microsoft.EntityFrameworkCore; + +namespace Lab.Snapshot.WebAPI; + +public class MemberRepository +{ + private readonly IDbContextFactory _memberDbContextFactory; + private readonly IMapper _mapper; + private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly IContextGetter _contextGetter; + + public MemberRepository(IDbContextFactory memberDbContextFactory, + IMapper mapper, + JsonSerializerOptions jsonSerializerOptions, + IContextGetter contextGetter) + { + this._memberDbContextFactory = memberDbContextFactory; + this._mapper = mapper; + this._jsonSerializerOptions = jsonSerializerOptions; + this._contextGetter = contextGetter; + } + + public async Task<(Failure Failure, bool Data)> InsertMemberAsync(InsertMemberRequest request) + { + var authContext = this._contextGetter.Get(); + var search = $"[{{\"Id\": \"{request.Account.Id}\"}}]"; + + await using var dbContext = await this._memberDbContextFactory.CreateDbContextAsync(); + var accountExist = dbContext.Members.AsNoTracking().Any(p => EF.Functions.JsonContains(p.Accounts, search)); + if (accountExist) + { + return (new Failure(FailureCode.MemberExist, $"Member({request.Account.Id}) exist"), false); + } + + var member = new MemberDataEntity + { + Id = Guid.NewGuid().ToString(), + Profile = this._mapper.Map(request.Profile), + Accounts = new List + { + this._mapper.Map(request.Account) + }, + CreatedAt = authContext.Now, + CreatedBy = authContext.UserId, + UpdatedAt = authContext.Now, + UpdatedBy = authContext.UserId, + Version = 1 + }; + var snapshot = new SnapshotDataEntity + { + Id = member.Id, + DataType = typeof(MemberDataEntity).ToString(), + Data = JsonNode.Parse(JsonSerializer.Serialize(member, this._jsonSerializerOptions)), + DataFormat = DataFormat.Full.ToString(), + CreatedAt = authContext.Now, + CreatedBy = authContext.UserId, + Version = member.Version + }; + await dbContext.Members.AddAsync(member); + await dbContext.Snapshots.AddAsync(snapshot); + await dbContext.SaveChangesAsync(); + return (null, true); + } + + public async Task<(Failure Failure, bool Data)> BindMemberAsync(string currentAccount, + UpdateMemberRequest request) + { + var authContext = this._contextGetter.Get(); + var search = $"[{{\"Id\": \"{currentAccount}\"}}]"; + + await using var dbContext = await this._memberDbContextFactory.CreateDbContextAsync(); + var oldMember = await dbContext.Members.FirstOrDefaultAsync(p => EF.Functions.JsonContains(p.Accounts, search)); + if (oldMember == null) + { + return (new Failure(FailureCode.MemberNotExist, $"Member({currentAccount}) not exist"), false); + } + + var newMember = this.DeepClone(oldMember); + + this.UpdateAccounts(request.Accounts, newMember); + this.UpdateProfile(request.Profile, newMember); + + // 比對兩個 member 內容是否有差異 + var diffResult = this.Diff(oldMember, newMember); + if (diffResult.Result == false) + { + // 沒有差異,不做任何事 + return (null, true); + } + + // 有差異,進版號 + newMember.UpdatedAt = authContext.Now; + newMember.UpdatedBy = authContext.UserId; + newMember.Version = oldMember.Version + 1; + + // 產生差異內容 + var diffData = diffResult.Data; + + var snapshot = new SnapshotDataEntity + { + Id = newMember.Id, + DataType = typeof(MemberDataEntity).ToString(), + Data = diffData, + DataFormat = DataFormat.Diff.ToString(), + CreatedAt = newMember.UpdatedAt, + CreatedBy = newMember.UpdatedBy, + Version = newMember.Version + }; + + // 更新時,可以使用樂觀鎖定,Update Where Version=oldMember.Version,或是悲觀鎖,這裡我沒有實作 + var entry = dbContext.Members.Entry(oldMember); + entry.CurrentValues.SetValues(newMember); + + await dbContext.Snapshots.AddAsync(snapshot); + await dbContext.SaveChangesAsync(); + return (null, true); + } + + private (JsonNode Data, bool Result) Diff(MemberDataEntity oldMember, MemberDataEntity newMember) + { + var oldData = JsonSerializer.Serialize(oldMember, this._jsonSerializerOptions); + var newData = JsonSerializer.Serialize(newMember, this._jsonSerializerOptions); + var diff = JsonDiffPatcher.Diff(oldData, newData, + new JsonDiffOptions + { + JsonElementComparison = JsonElementComparison.RawText, + }); + + if (diff == null) + { + return (null, false); + } + + return (diff, true); + } + + private MemberDataEntity UpdateAccounts(List srcAccounts, MemberDataEntity destMember) + { + foreach (var srcAccount in srcAccounts) + { + var accountExist = false; + foreach (var destAccount in destMember.Accounts) + { + if (srcAccount.Id == destAccount.Id) + { + accountExist = true; + break; + } + } + + // 帳號不存在才加入 + if (accountExist == false) + { + var map = this._mapper.Map(srcAccount); + destMember.Accounts.Add(map); + } + } + + return destMember; + } + + private void UpdateProfile(ServiceModels.Profile srcProfile, MemberDataEntity destMember) + { + if (srcProfile.Equals(destMember.Profile)) + { + return; + } + + destMember.Profile = this._mapper.Map(srcProfile); + } + + public MemberDataEntity DeepClone(MemberDataEntity oldMember) + { + var newMember = oldMember with + { + Accounts = this._mapper.Map>(oldMember.Accounts), + Profile = this._mapper.Map(oldMember.Profile) + }; + return newMember; + } + + public async Task QueryMemberByAccountAsync(string account, int? version) + { + var search = $"[{{\"Id\": \"{account}\"}}]"; + await using var dbContext = await this._memberDbContextFactory.CreateDbContextAsync(); + + // 讀取最新資料 + var queryable = from member in dbContext.Members + where EF.Functions.JsonContains(member.Accounts, search) + select new { member }; + var latestMember = await queryable.AsNoTracking().FirstOrDefaultAsync(); + if (latestMember == null) + { + return null; + } + + if (version.HasValue == false) + { + return this._mapper.Map(latestMember.member); + } + + // 讀取快照 + var query = dbContext.Snapshots + .Where(p => p.Id == latestMember.member.Id) + .Where(p => p.Version <= version.Value) + ; + var snapshots = await query.AsNoTracking().ToListAsync(); + JsonNode finial = null; + + // 依序合併快照 + foreach (var snapshot in snapshots) + { + if (snapshot.Version == 1) + { + finial = snapshot.Data; + continue; + } + + JsonDiffPatcher.Patch(ref finial, snapshot.Data); + } + + // search account in JsonNode + if (finial == null) + { + return null; + } + + var lastSnapshot = snapshots.Last(); + var result = finial.Deserialize(this._jsonSerializerOptions); + result.CreatedAt = latestMember.member.CreatedAt; + result.CreatedBy = latestMember.member.CreatedBy; + result.UpdatedAt = lastSnapshot.CreatedAt; + result.UpdatedBy = lastSnapshot.CreatedBy; + result.Version = lastSnapshot.Version; + return result; + } + + public async Task GetMemberAsync(string id, int? version) + { + await using var dbContext = await this._memberDbContextFactory.CreateDbContextAsync(); + if (version.HasValue == false) + { + var query = dbContext.Members.Where(p => p.Id == id) + ; + var data = await query.AsNoTracking().FirstOrDefaultAsync(); + if (data == null) + { + return null; + } + + return this._mapper.Map(data); + } + else + { + var query = dbContext.Snapshots.Where(p => p.Id == id) + .Where(p => p.Version <= version.Value) + ; + var snapshots = await query.AsNoTracking().ToListAsync(); + + JsonNode finial = null; + + // 依序合併快照 + foreach (var snapshot in snapshots) + { + if (snapshot.Version == 1) + { + finial = snapshot.Data; + continue; + } + + JsonDiffPatcher.Patch(ref finial, snapshot.Data); + } + + if (finial == null) + { + return null; + } + + return finial.Deserialize(this._jsonSerializerOptions); + } + } + + public async Task> GetMembersAsync() + { + await using var dbContext = await this._memberDbContextFactory.CreateDbContextAsync(); + var data = await dbContext.Members.AsNoTracking().ToListAsync(); + return this._mapper.Map>(data); + } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Program.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Program.cs new file mode 100644 index 00000000..bd7bc100 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Program.cs @@ -0,0 +1,83 @@ +using Lab.Snapshot.DB; +using Lab.Snapshot.WebAPI; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers() + .AddJsonOptions(options => JsonSerializeFactory.Apply(options.JsonSerializerOptions)) + ; + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// builder.Services.AddScoped(p => new AuthContext +// { +// TraceId = Guid.NewGuid().ToString(), +// Now = DateTimeOffset.UtcNow, +// UserId = "System-" + Guid.NewGuid().ToString() +// }); + +builder.Services.AddSingleton>(); +builder.Services.AddSingleton>(p => p.GetService>()); +builder.Services.AddSingleton>(p => p.GetService>()); + +builder.Services.AddSingleton(_ => JsonSerializeFactory.Init()); +builder.Services.AddSingleton(); +builder.Services.AddAutoMapper(typeof(Mapper)); +ConfigDb(builder.Services); + +var app = builder.Build(); + +app.Use(async (context, next) => +{ + var contextSetter = context.RequestServices.GetService>(); + var authContext = new AuthContext + { + TraceId = Guid.NewGuid().ToString(), + Now = DateTimeOffset.UtcNow, + UserId = "System" + }; + contextSetter.Set(authContext); + context.Response.Headers.Add("X-Trace-Id", authContext.TraceId); + + await next.Invoke(); +}); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); + +static void ConfigDb(IServiceCollection services) +{ + //依照真實情況注入連線字串 + Environment.SetEnvironmentVariable(EnvironmentNames.DbConnectionString, + EnvironmentNames.Values[EnvironmentNames.DbConnectionString]); + + services.AddSingleton(p => { return LoggerFactory.Create(builder => { builder.AddConsole(); }); }); + services.AddDbContextFactory((p, options) => + { + //依照真實情況取得連線字串 + var connectionString = Environment.GetEnvironmentVariable(EnvironmentNames.DbConnectionString); + options.UseNpgsql(connectionString, + builder => builder.EnableRetryOnFailure( + 10, + TimeSpan.FromSeconds(30), + new List { "57P01" })) + ; + }); +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Properties/launchSettings.json b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Properties/launchSettings.json new file mode 100644 index 00000000..fc68fbb9 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56731", + "sslPort": 44307 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5060", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7176;http://localhost:5060", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/Account.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/Account.cs new file mode 100644 index 00000000..262f9ae9 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/Account.cs @@ -0,0 +1,8 @@ +namespace Lab.Snapshot.WebAPI.ServiceModels; + +public class Account +{ + public string Id { get; set; } + + public string Type { get; set; } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/InsertMemberRequest.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/InsertMemberRequest.cs new file mode 100644 index 00000000..4d6ebd53 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/InsertMemberRequest.cs @@ -0,0 +1,15 @@ +namespace Lab.Snapshot.WebAPI.ServiceModels; + +public class InsertMemberRequest +{ + public Account Account { get; set; } + + public Profile Profile { get; set; } +} + +public class UpdateMemberRequest +{ + public List Accounts { get; set; } + + public Profile Profile { get; set; } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/MemberResponse.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/MemberResponse.cs new file mode 100644 index 00000000..f913b165 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/MemberResponse.cs @@ -0,0 +1,20 @@ +namespace Lab.Snapshot.WebAPI.ServiceModels; + +public class MemberResponse +{ + public string Id { get; set; } + + public Profile Profile { get; set; } + + public List Accounts { 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/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/Profile.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/Profile.cs new file mode 100644 index 00000000..7bcb57c7 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/ServiceModels/Profile.cs @@ -0,0 +1,8 @@ +namespace Lab.Snapshot.WebAPI.ServiceModels; + +public class Profile +{ + public int Age { get; set; } + + public string Name { get; set; } +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/SystemClock.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/SystemClock.cs new file mode 100644 index 00000000..df537255 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/SystemClock.cs @@ -0,0 +1,6 @@ +namespace Lab.Snapshot.WebAPI; + +public class SystemClock: ISystemClock +{ + public DateTimeOffset Now => DateTimeOffset.UtcNow; +} \ No newline at end of file diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/WeatherForecast.cs b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/WeatherForecast.cs new file mode 100644 index 00000000..1cc8c31e --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Lab.Snapshot.WebAPI; + +public class WeatherForecast +{ + public DateOnly 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/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/appsettings.Development.json b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/appsettings.json b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.WebAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Shnapshot/Lab.Snapshot/Lab.Snapshot.sln b/Shnapshot/Lab.Snapshot/Lab.Snapshot.sln new file mode 100644 index 00000000..e989cbc8 --- /dev/null +++ b/Shnapshot/Lab.Snapshot/Lab.Snapshot.sln @@ -0,0 +1,33 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Snapshot.Test", "Lab.Snapshot.Test\Lab.Snapshot.Test.csproj", "{BEA4D8AA-807A-46FE-A660-116F3D5F3BC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Snapshot.DB", "Lab.Snapshot.DB\Lab.Snapshot.DB.csproj", "{472E967C-E725-494F-B906-A60F8B206895}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Snapshot.WebAPI", "Lab.Snapshot.WebAPI\Lab.Snapshot.WebAPI.csproj", "{2DC3F6CC-DBE5-4539-81EC-7368CA927387}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A1B3C0E5-A98D-4257-A5FC-22CD9720A0EE}" + 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 + {BEA4D8AA-807A-46FE-A660-116F3D5F3BC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEA4D8AA-807A-46FE-A660-116F3D5F3BC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEA4D8AA-807A-46FE-A660-116F3D5F3BC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEA4D8AA-807A-46FE-A660-116F3D5F3BC5}.Release|Any CPU.Build.0 = Release|Any CPU + {472E967C-E725-494F-B906-A60F8B206895}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {472E967C-E725-494F-B906-A60F8B206895}.Debug|Any CPU.Build.0 = Debug|Any CPU + {472E967C-E725-494F-B906-A60F8B206895}.Release|Any CPU.ActiveCfg = Release|Any CPU + {472E967C-E725-494F-B906-A60F8B206895}.Release|Any CPU.Build.0 = Release|Any CPU + {2DC3F6CC-DBE5-4539-81EC-7368CA927387}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DC3F6CC-DBE5-4539-81EC-7368CA927387}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DC3F6CC-DBE5-4539-81EC-7368CA927387}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DC3F6CC-DBE5-4539-81EC-7368CA927387}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Shnapshot/Lab.Snapshot/docker-compose.yml b/Shnapshot/Lab.Snapshot/docker-compose.yml new file mode 100644 index 00000000..a760b5eb --- /dev/null +++ b/Shnapshot/Lab.Snapshot/docker-compose.yml @@ -0,0 +1,8 @@ +services: + db: + image: postgres + container_name: postgres-latest + environment: + - POSTGRES_PASSWORD=guest + ports: + - 5432:5432 \ No newline at end of file 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.ConsoleApp/Lab.SerilogProject.ConsoleApp.csproj b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.ConsoleApp/Lab.SerilogProject.ConsoleApp.csproj new file mode 100644 index 00000000..f2c6b3cb --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.ConsoleApp/Lab.SerilogProject.ConsoleApp.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + diff --git a/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.ConsoleApp/LabBackgroundService.cs b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.ConsoleApp/LabBackgroundService.cs new file mode 100644 index 00000000..10ed3f86 --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.ConsoleApp/LabBackgroundService.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Lab.SerilogProject.ConsoleApp; + +public class LabBackgroundService : BackgroundService +{ + private readonly ILogger _logger; + + public LabBackgroundService(ILogger logger) + { + this._logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + this._logger.LogInformation(new EventId(2000, "Trace"), "Start {ClassName}.{MethodName}...", + nameof(LabBackgroundService), nameof(this.ExecuteAsync)); + + var sensorInput = new { Latitude = 25, Longitude = 134 }; + this._logger.LogInformation("Processing {@SensorInput}", sensorInput); + } +} \ No newline at end of file diff --git a/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.ConsoleApp/Program.cs b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.ConsoleApp/Program.cs new file mode 100644 index 00000000..3c93f5fa --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.ConsoleApp/Program.cs @@ -0,0 +1,49 @@ +using Lab.SerilogProject.ConsoleApp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Formatting.Json; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Day) + .CreateBootstrapLogger() + ; + +try +{ + Log.Information("Starting host"); + + var builder = Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddHostedService(); + }) + .UseSerilog((context, services, config) => + { + var formatter = new JsonFormatter(); + + config.ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console(formatter) + .WriteTo.Seq("http://localhost:5341") + .WriteTo.File(formatter, "logs/app-.txt", rollingInterval: RollingInterval.Minute); + }); + ; + var host = builder.Build(); + host.StartAsync(); + host.StopAsync(); + Console.WriteLine("Bye~~~"); +} +catch (Exception ex) +{ + Log.Fatal(ex, "Host terminated unexpectedly"); + throw; +} +finally +{ + Log.CloseAndFlush(); +} \ 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..2a894e7d --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Controllers/WeatherForecastController.cs @@ -0,0 +1,45 @@ +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) + { + this._logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + // using var scope = this._logger.BeginScope(new Dictionary + // { + // ["UserId"] = "svrooij", + // ["OperationType"] = "update", + // }); + + // UserId and OperationType are set for all logging events in these brackets + this._logger.LogInformation(new EventId(2000, "Trace"), "Start {ControllerName}.{MethodName}...", + nameof(WeatherForecastController), nameof(this.Get)); + + var sensorInput = new { Latitude = 25, Longitude = 134 }; + this._logger.LogInformation("Processing {@SensorInput}", sensorInput); + + 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..1524f55b --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Lab.SerilogProject.WebApi.csproj @@ -0,0 +1,20 @@ + + + + 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..111d31f7 --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/Program.cs @@ -0,0 +1,81 @@ +using Lab.SerilogProject.WebApi; +using Serilog; +using Serilog.Events; +using Serilog.Formatting.Json; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Day) + .CreateBootstrapLogger() + ; +try +{ + Log.Information("Starting web host"); + + var builder = WebApplication.CreateBuilder(args); + + // builder.Host.UseSerilog(); //<=== 讓 Host 使用 Serilog + var formatter = new JsonFormatter(); + // var formatter = new MessageTemplateTextFormatter(); + // var formatter = new RawFormatter(); + // var formatter = new RenderedCompactJsonFormatter(); + // var formatter = new CompactJsonFormatter(); + // var formatter = new ExpressionTemplate( + // "{ {_t: @t, _msg: @m, _props: @p} }\n"); + builder.Host.UseSerilog((context, services, config) => + { + config.ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console(formatter) + .WriteTo.Seq("http://localhost:5341") + .WriteTo.File(formatter, "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.UseMiddleware(); + app.UseHttpsRedirection(); + // app.UseSerilogRequestLogging(); //<=== 每一個 Request 使用 Serilog 記錄下來 + app.UseSerilogRequestLogging(options => + { + options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => + { + diagnosticContext.Set("UserId", "svrooij"); + diagnosticContext.Set("OperationType", "update"); + }; + }); + 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/TraceMiddleware.cs b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/TraceMiddleware.cs new file mode 100644 index 00000000..8438d65c --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.WebApi/TraceMiddleware.cs @@ -0,0 +1,28 @@ +namespace Lab.SerilogProject.WebApi; + +public class TraceMiddleware +{ + private readonly RequestDelegate _next; + + public TraceMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context, ILogger logger) + { + using (logger.BeginScope(new Dictionary + { + ["UserId"] = "svrooij", + ["OperationType"] = "update", + })) + { + await this._next.Invoke(context); + } + + // using (logger.BeginScope("{_rid}", Guid.NewGuid())) + // { + // await this._next.Invoke(context); + // } + } +} 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..ba64d262 --- /dev/null +++ b/StructLog/Lab.SerilogProject/src/Lab.SerilogProject/Lab.SerilogProject.sln @@ -0,0 +1,28 @@ + +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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.SerilogProject.ConsoleApp", "Lab.SerilogProject.ConsoleApp\Lab.SerilogProject.ConsoleApp.csproj", "{6BFCDC2D-787D-466B-BB43-70DABD0E9B1F}" +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 + {6BFCDC2D-787D-466B-BB43-70DABD0E9B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BFCDC2D-787D-466B-BB43-70DABD0E9B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BFCDC2D-787D-466B-BB43-70DABD0E9B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BFCDC2D-787D-466B-BB43-70DABD0E9B1F}.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 jmeter sample/Taskfile.yml b/Test/Lab jmeter sample/Taskfile.yml new file mode 100644 index 00000000..ce7f3bf6 --- /dev/null +++ b/Test/Lab jmeter sample/Taskfile.yml @@ -0,0 +1,18 @@ +# Taskfile.yml + +version: "3" + +dotenv: [ "secrets/secrets.env" ] + +tasks: + clear-log: + cmds: + - nu -c 'rm -rf ./jmeter.log' + - nu -c 'rm -rf ./temp' + + first: + desc: first sample + cmds: + - task: clear-log + - nu -c 'rm -rf ./first' + - jmeter -n -t first.jmx -l ./first/result.jtl -e -o ./first diff --git a/Test/Lab jmeter sample/first.jmx b/Test/Lab jmeter sample/first.jmx new file mode 100644 index 00000000..f460b973 --- /dev/null +++ b/Test/Lab jmeter sample/first.jmx @@ -0,0 +1,437 @@ + + + + + + false + true + false + + + + + + + + + + currentFolder + ${__BeanShell(import org.apache.jmeter.services.FileServer; FileServer.getFileServer().getBaseDir())} + = + + + currentTestPlanFileName + ${__BeanShell(import org.apache.jmeter.services.FileServer;import org.apache.commons.io.FilenameUtils; FilenameUtils.getBaseName(FileServer.getFileServer().getScriptName()))} + = + + + + + + groovy + + + true + import org.apache.jmeter.util.JMeterUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.FileUtils; + +log.warn("currentFolder: " + vars.get("currentFolder")); +log.warn("currentTestPlanFileName: " + vars.get("currentTestPlanFileName")); + + + + + + 30 + 0 + 10 + 300 + 0 + + + + false + -1 + + stoptest + + + + + allowedThroughputSurplus + 1.0 + 0.0 + + 1 + 0 + 300 + 10000 + 0 + + throughput + 35.0 + 0.0 + + 1 + + + + + + + test.k6.io + 80 + http + + + GET + true + false + false + false + + + + + + + false + false + + + false + 2 + + + 60000 + 600 + 800 + + false + 150 + + + ${currentFolder}\\${currentTestPlanFileName}\graphs + true + true + false + false + ${currentFolder}\\${currentTestPlanFileName}\result.jtl + + + + + + + true + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + ${currentFolder}\\${currentTestPlanFileName}\View Results Tree.csv + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + ${currentFolder}\\${currentTestPlanFileName}\Summary Report.csv + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + 500 + false + + + + + false + false + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + 1000 + false + + + + + false + false + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + 500 + false + + + + + false + false + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + 1000 + false + + + + + false + false + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + 500 + false + + + + + false + false + + + jp@gc - Response Times Over Time + jp@gc - Transactions per Second + + + Overall Response Times + Successful Transactions per Second + + + + + + + diff --git a/Test/Lab k6 sample/1.js b/Test/Lab k6 sample/1.js new file mode 100644 index 00000000..0f24eabc --- /dev/null +++ b/Test/Lab k6 sample/1.js @@ -0,0 +1,7 @@ +import http from 'k6/http'; +import {sleep} from 'k6'; + +export default function () { + http.get('http://test.k6.io'); + sleep(1); +} \ No newline at end of file diff --git a/Test/Lab k6 sample/data/users.json b/Test/Lab k6 sample/data/users.json new file mode 100644 index 00000000..c3bda56a --- /dev/null +++ b/Test/Lab k6 sample/data/users.json @@ -0,0 +1,14 @@ +[ + { + "username": "user1", + "password": "password1" + }, + { + "username": "user2", + "password": "password2" + }, + { + "username": "user3", + "password": "password3" + } +] diff --git a/Test/Lab k6 sample/read-json-file.js b/Test/Lab k6 sample/read-json-file.js new file mode 100644 index 00000000..ed03a89e --- /dev/null +++ b/Test/Lab k6 sample/read-json-file.js @@ -0,0 +1,16 @@ +import {SharedArray} from 'k6/data'; +import {sleep} from 'k6'; + +const data = new SharedArray('users', function () { + // const d = open('D:\\src\\sample.dotblog\\Test\\Lab k6 sample\\users.json'); + // here you can open files, and then do additional processing or generate the array with data dynamically + const f = JSON.parse(open('./data/users.json')); + return f; // f must be an array[] +}); + +export default () => { + console.log('Getting random user...'); + const randomUser = data[Math.floor(Math.random() * data.length)]; + console.log(`${randomUser.username}, ${randomUser.password}`); + sleep(3); +}; diff --git a/Test/Lab k6 sample/set-env.js b/Test/Lab k6 sample/set-env.js new file mode 100644 index 00000000..4f867c7b --- /dev/null +++ b/Test/Lab k6 sample/set-env.js @@ -0,0 +1,8 @@ +import http from 'k6/http'; +import { sleep } from 'k6'; + +export default function () { + console.log(`User agent is '${__ENV.MY_USER_AGENT}'`); + http.get('http://test.k6.io'); + sleep(1); +} diff --git a/Test/Lab k6 sample/set-sys-env.js b/Test/Lab k6 sample/set-sys-env.js new file mode 100644 index 00000000..201967e1 --- /dev/null +++ b/Test/Lab k6 sample/set-sys-env.js @@ -0,0 +1,8 @@ +import http from 'k6/http'; +import { sleep } from 'k6'; + +export default function () { + console.log(`User agent is '${__ENV.ASPNETCORE_ENVIRONMENT }'`); + http.get('http://test.k6.io'); + sleep(1); +} diff --git a/Test/Lab k6 sample/to-html-report.js b/Test/Lab k6 sample/to-html-report.js new file mode 100644 index 00000000..2597bbef --- /dev/null +++ b/Test/Lab k6 sample/to-html-report.js @@ -0,0 +1,18 @@ +import {htmlReport} from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js"; +import {textSummary} from "https://jslib.k6.io/k6-summary/0.0.1/index.js"; + +import http from 'k6/http'; +import {sleep} from 'k6'; + +export default function () { + console.log(`User agent is '${__ENV.MY_USER_AGENT}'`); + http.get('http://test.k6.io'); + sleep(1); +} + +export function handleSummary(data) { + return { + "result.html": htmlReport(data), + stdout: textSummary(data, { indent: " ", enableColors: true }), + }; +} \ 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/Lab.Test.Container/.dockerignore b/Test/Lab.Test.Container/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/Test/Lab.Test.Container/.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/Test/Lab.Test.Container/Lab.Test.Container.sln b/Test/Lab.Test.Container/Lab.Test.Container.sln new file mode 100644 index 00000000..040dc3d4 --- /dev/null +++ b/Test/Lab.Test.Container/Lab.Test.Container.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.TestContainers.Test", "Lab.TestContainers.Test\Lab.TestContainers.Test.csproj", "{71FF0299-AB51-4871-A05F-162C9DEC3790}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.TestContainers.WebApi", "Lab.TestContainers.WebApi\Lab.TestContainers.WebApi.csproj", "{DD417E0D-2DCD-4AA8-8E8C-C05E32AD7CF4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {71FF0299-AB51-4871-A05F-162C9DEC3790}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71FF0299-AB51-4871-A05F-162C9DEC3790}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71FF0299-AB51-4871-A05F-162C9DEC3790}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71FF0299-AB51-4871-A05F-162C9DEC3790}.Release|Any CPU.Build.0 = Release|Any CPU + {DD417E0D-2DCD-4AA8-8E8C-C05E32AD7CF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD417E0D-2DCD-4AA8-8E8C-C05E32AD7CF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD417E0D-2DCD-4AA8-8E8C-C05E32AD7CF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD417E0D-2DCD-4AA8-8E8C-C05E32AD7CF4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Test/Lab.Test.Container/Lab.TestContainers.Test/Lab.TestContainers.Test.csproj b/Test/Lab.Test.Container/Lab.TestContainers.Test/Lab.TestContainers.Test.csproj new file mode 100644 index 00000000..a2a4c70e --- /dev/null +++ b/Test/Lab.Test.Container/Lab.TestContainers.Test/Lab.TestContainers.Test.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + + + + + diff --git a/Test/Lab.Test.Container/Lab.TestContainers.Test/UnitTest1.cs b/Test/Lab.Test.Container/Lab.TestContainers.Test/UnitTest1.cs new file mode 100644 index 00000000..0f8eadf3 --- /dev/null +++ b/Test/Lab.Test.Container/Lab.TestContainers.Test/UnitTest1.cs @@ -0,0 +1,101 @@ +using System.Data.Common; +using System.Diagnostics; +using DotNet.Testcontainers.Builders; +using Npgsql; +using Testcontainers.PostgreSql; + +namespace Lab.TestContainers.Test; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public async Task GenericContainer() + { + var waitStrategy = Wait.ForUnixContainer().UntilCommandIsCompleted("pg_isready"); + var postgreSqlContainer = new ContainerBuilder() + .WithImage("postgres:12-alpine") + .WithName("postgres.12") + .WithPortBinding(5432) + .WithWaitStrategy(waitStrategy) + .WithEnvironment("POSTGRES_USER", "postgres") + .WithEnvironment("POSTGRES_PASSWORD", "postgres") + .Build(); + await postgreSqlContainer.StartAsync() + .ConfigureAwait(false); + + var connectionString = "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=postgres"; + await using DbConnection connection = new NpgsqlConnection(connectionString); + await using DbCommand command = new NpgsqlCommand(); + await connection.OpenAsync(); + command.Connection = connection; + command.CommandText = "SELECT 1"; + var reader = await command.ExecuteReaderAsync(); + while (true) + { + var hasData = await reader.ReadAsync(); + if (hasData == false) + { + break; + } + + var value = reader.GetValue(0); + } + } + + [TestMethod] + public async Task ModuleContainer() + { + var waitStrategy = Wait.ForUnixContainer().UntilCommandIsCompleted("pg_isready"); + var postgreSqlContainer = new PostgreSqlBuilder() + .WithImage("postgres:12-alpine") + .WithName("postgres.12") + .WithPortBinding(5432, assignRandomHostPort: true) + .WithWaitStrategy(waitStrategy) + .WithUsername("postgres") + .WithPassword("postgres") + .Build(); + await postgreSqlContainer.StartAsync() + .ConfigureAwait(false); + + var connectionString = postgreSqlContainer.GetConnectionString(); + await using DbConnection connection = new NpgsqlConnection(connectionString); + await using DbCommand command = new NpgsqlCommand(); + await connection.OpenAsync(); + command.Connection = connection; + command.CommandText = "SELECT 1"; + } + + [TestMethod] + public async Task CreateImage() + { + var solutionDirectory = CommonDirectoryPath.GetSolutionDirectory(); + var dockerFilePath = "Lab.TestContainers.WebApi/Dockerfile"; + var imageBuilder = new ImageFromDockerfileBuilder() + .WithDockerfileDirectory(solutionDirectory, string.Empty) + .WithDockerfile(dockerFilePath) + .WithName("my.aspnet.core.7") + .Build(); + + await imageBuilder.CreateAsync() + .ConfigureAwait(false); + var container = new ContainerBuilder() + .WithImage("my.aspnet.core.7") + .WithName("my.aspnet.core.7") + .WithPortBinding(80) + .Build() + ; + await container.StartAsync() + .ConfigureAwait(false); + ; + var scheme = "http"; + var host = "localhost"; + var port = container.GetMappedPublicPort(80); + var url = "demo"; + + var httpClient = new HttpClient(); + var requestUri = new UriBuilder(scheme, host, port, url).Uri; + var actual = await httpClient.GetStringAsync(requestUri); + Assert.AreEqual("OK~", actual); + } +} \ No newline at end of file diff --git a/Test/Lab.Test.Container/Lab.TestContainers.Test/Usings.cs b/Test/Lab.Test.Container/Lab.TestContainers.Test/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/Test/Lab.Test.Container/Lab.TestContainers.Test/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Controllers/DemoController.cs b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Controllers/DemoController.cs new file mode 100644 index 00000000..7d4b2e3f --- /dev/null +++ b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Controllers/DemoController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.Test.Container.WebApi.Controllers; + +[ApiController] +[Route("[controller]")] +public class DemoController : ControllerBase +{ + private readonly ILogger _logger; + + public DemoController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetDemo")] + public ActionResult Get() + { + return this.Ok("OK~"); + } +} \ No newline at end of file diff --git a/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Dockerfile b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Dockerfile new file mode 100644 index 00000000..00f09879 --- /dev/null +++ b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /src +COPY ["Lab.TestContainers.WebApi/Lab.TestContainers.WebApi.csproj", "Lab.TestContainers.WebApi/"] +RUN dotnet restore "Lab.TestContainers.WebApi/Lab.TestContainers.WebApi.csproj" +COPY . . +WORKDIR "/src/Lab.TestContainers.WebApi" +RUN dotnet build "Lab.TestContainers.WebApi.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Lab.TestContainers.WebApi.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Lab.TestContainers.WebApi.dll"] diff --git a/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Lab.TestContainers.WebApi.csproj b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Lab.TestContainers.WebApi.csproj new file mode 100644 index 00000000..a88411cf --- /dev/null +++ b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Lab.TestContainers.WebApi.csproj @@ -0,0 +1,22 @@ + + + + net7.0 + enable + enable + Lab.Test.Container.WebApi + Linux + + + + + + + + + + .dockerignore + + + + diff --git a/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Program.cs b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Program.cs new file mode 100644 index 00000000..329fe361 --- /dev/null +++ b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Program.cs @@ -0,0 +1,26 @@ +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 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/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Properties/launchSettings.json b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Properties/launchSettings.json new file mode 100644 index 00000000..553487f8 --- /dev/null +++ b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:18819", + "sslPort": 44337 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5214", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7023;http://localhost:5214", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Test/Lab.Test.Container/Lab.TestContainers.WebApi/WeatherForecast.cs b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/WeatherForecast.cs new file mode 100644 index 00000000..0c88c8fa --- /dev/null +++ b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Lab.Test.Container.WebApi; + +public class WeatherForecast +{ + public DateOnly 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/Test/Lab.Test.Container/Lab.TestContainers.WebApi/appsettings.Development.json b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Test/Lab.Test.Container/Lab.TestContainers.WebApi/appsettings.json b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Test/Lab.Test.Container/Lab.TestContainers.WebApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} 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/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/Calculation.cs b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/Calculation.cs new file mode 100644 index 00000000..b73f09db --- /dev/null +++ b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/Calculation.cs @@ -0,0 +1,9 @@ +namespace Lab.LivingDocs.Test; + +public class Calculation +{ + public double Add(double firstNumber, double secondNumber) + { + return firstNumber + secondNumber; + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/Lab.LivingDocs.Test.csproj b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/Lab.LivingDocs.Test.csproj new file mode 100644 index 00000000..6ccc2f40 --- /dev/null +++ b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/Lab.LivingDocs.Test.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + diff --git a/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/UnitTest1.cs b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/UnitTest1.cs new file mode 100644 index 00000000..ff4202d1 --- /dev/null +++ b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Lab.LivingDocs.Test; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void TestMethod1() + { + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/Usings.cs b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git "a/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/\350\250\210\347\256\227\346\251\237.feature" "b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/\350\250\210\347\256\227\346\251\237.feature" new file mode 100644 index 00000000..998d0f2e --- /dev/null +++ "b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/\350\250\210\347\256\227\346\251\237.feature" @@ -0,0 +1,20 @@ +Feature: 計算機 +Simple calculator for adding two numbers + + 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/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/\350\250\210\347\256\227\346\251\237Step.cs" "b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/\350\250\210\347\256\227\346\251\237Step.cs" new file mode 100644 index 00000000..4f830d23 --- /dev/null +++ "b/Test/Specflow3/Lab.LivingDocs/Lab.LivingDocs.Test/\350\250\210\347\256\227\346\251\237Step.cs" @@ -0,0 +1,36 @@ +using TechTalk.SpecFlow; + +namespace Lab.LivingDocs.Test; + +[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/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/BaseStep.cs b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/BaseStep.cs new file mode 100644 index 00000000..7b5b0ccc --- /dev/null +++ b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/BaseStep.cs @@ -0,0 +1,151 @@ +using System.Text.Json; +using FluentAssertions; +using TechTalk.SpecFlow; +using TechTalk.SpecFlow.Assist; + +namespace Lab.SpecflowCreateAndCompareJson; + +[Binding] +public class BaseStep +{ + [Given(@"已準備 Member 資料\(錯誤\)")] + public void Given已準備Member資料錯誤(Table table) + { + var members = table.CreateSet(row => + { + var member = new Member + { + Id = null, + Name = null, + IpData = null, + Orders = null + }; + + return member; + }); + } + + [Given(@"已準備 Member 資料\(正確\)")] + public void Given已準備Member資料正確(Table table) + { + var members = new List(); + foreach (var row in table.Rows) + { + var member = new Member(); + foreach (var header in table.Header) + { + switch (header) + { + case nameof(Member.Id): + member.Id = row[header]; + break; + case nameof(Member.Age): + member.Age = int.Parse(row[header]); + break; + case nameof(Member.State): + member.State = Enum.Parse(row[header]); + break; + case nameof(Member.Name): + member.Name = TryGetName(row); + break; + case nameof(Member.IpData): + member.IpData = TryGetIpData(row); + break; + case nameof(Member.Orders): + member.Orders = TryGetOrders(row); + break; + } + } + + members.Add(member); + } + } + + private static Name? TryGetName(TableRow row) + { + var data = row.TryGetValue(nameof(Member.Name), out var name) + ? JsonSerializer.Deserialize(name) + : null; + return data; + } + + private static List? TryGetOrders(TableRow row) + { + var data = row.TryGetValue(nameof(Member.Orders), out var orders) + ? JsonSerializer.Deserialize>(orders) + : new List(); + return data; + } + + private static List? TryGetIpData(TableRow row) + { + var data = row.TryGetValue(nameof(Member.IpData), out var ip) + ? JsonSerializer.Deserialize>(ip) + : new List(); + return data; + } + + [Given(@"已準備 Member 資料\(擴充方法\)")] + public void Given已準備Member資料擴充方法(Table table) + { + var members = table.CreateJsonSet(); + } + + [Then(@"預期得到 Member 資料\(錯誤\)")] + public void Then預期得到Member資料錯誤(Table table) + { + var actual = CreateActualMembers(); + table.CompareToSet(actual); + } + + [Then(@"預期得到 Member 資料\(正確\)")] + public void Then預期得到Member資料正確(Table table) + { + var actual = CreateActualMembers(); + var expected = table.CreateJsonSet(); + var header = table.Header.ToHashSet(); + + // actual.Should().BeEquivalentTo(expected, options => options + // .Including(x => x.Id) + // .Including(x => x.Age) + // .Including(x => x.Name) + // .Including(x => x.State) + // .Including(x => x.IpData) + // .Including(x => x.Orders) + // ); + + actual.Should().BeEquivalentTo(expected, options => + { + options.Including(info => header.Contains(info.Name)); + if (header.Contains(nameof(Member.Name))) + { + options.Including(info => info.Name); + } + return options; + }); + + // actual.Should() + // .BeEquivalentTo(expected); + } + + private static List CreateActualMembers() + { + List actual = + [ + new Member + { + Id = "1", + Age = 18, + Name = new Name + { + FirstName = "yaochang", + LastName = "yu" + }, + State = State.Active, + IpData = ["192.168.0.1", "192.168.0.2"], + Orders = [new Order { Id = "123" }] + } + ]; + return actual; + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/GlobalUsings.cs b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/GlobalUsings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/Lab.SpecflowCreateAndCompareJson.csproj b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/Lab.SpecflowCreateAndCompareJson.csproj new file mode 100644 index 00000000..46472657 --- /dev/null +++ b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/Lab.SpecflowCreateAndCompareJson.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/Member.cs b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/Member.cs new file mode 100644 index 00000000..9ba339b6 --- /dev/null +++ b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/Member.cs @@ -0,0 +1,35 @@ +namespace Lab.SpecflowCreateAndCompareJson; + +public class Member +{ + public string Id { get; set; } + + public int Age { get; set; } + + public Name Name { get; set; } + + public State State { get; set; } + + public List IpData { get; set; } + + public List Orders { get; set; } +} + +public enum State +{ + None, + Active, + Inactive, +} + +public class Order +{ + public string Id { get; set; } +} + +public class Name +{ + public string FirstName { get; set; } + + public string LastName { get; set; } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/SpecFlowFeature1.feature b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/SpecFlowFeature1.feature new file mode 100644 index 00000000..4c1316e2 --- /dev/null +++ b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/SpecFlowFeature1.feature @@ -0,0 +1,23 @@ +Feature: SpecFlowFeature1 +Simple calculator for adding two numbers + + Scenario: 建立一筆會員(錯誤) + Given 已準備 Member 資料(錯誤) + | Id | Age | IpData | Orders | State | + | 1 | 18 | ["192.168.0.1","192.168.0.2"] | [{"Id":"123"}] | Active | + Then 預期得到 Member 資料(錯誤) + | Id | Age | IpData | Orders | State | + | 1 | 18 | ["192.168.0.1","192.168.0.2"] | [{"Id":"123"}] | Active | + + Scenario: 建立一筆會員(正確) + Given 已準備 Member 資料(正確) + | Id | Age | Name | IpData | Orders | State | + | 1 | 18 | {"FirstName":"yaochang","LastName":"yu"} | ["192.168.0.1","192.168.0.2"] | [{"Id":"123"}] | Active | + Then 預期得到 Member 資料(正確) + | Id | Age | Name | IpData | Orders | State | + | 1 | 18 | {"FirstName":"yaochang","LastName":"yu1"} | ["192.168.0.1","192.168.0.3"] | [{"Id":"124"}] | Active | + + Scenario: 建立一筆會員(擴充方法) + Given 已準備 Member 資料(擴充方法) + | Id | Age | Name | IpData | Orders | State | + | 1 | 18 | {"FirstName":"yaochang","LastName":"yu"} | ["192.168.0.1","192.168.0.2"] | [{"Id":"123"}] | Active | \ No newline at end of file diff --git a/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/TableExtensions.cs b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/TableExtensions.cs new file mode 100644 index 00000000..08ed68b7 --- /dev/null +++ b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowCreateAndCompareJson/TableExtensions.cs @@ -0,0 +1,64 @@ +using System.Text.Json; +using TechTalk.SpecFlow; + +namespace Lab.SpecflowCreateAndCompareJson; + +public static class TableExtensions +{ + public static IEnumerable? CreateJsonSet(this Table table) + { + var results = new List(); + var type = typeof(T); + foreach (var row in table.Rows) + { + var instance = Activator.CreateInstance(); + foreach (var header in table.Header) + { + var property = type.GetProperty(header); + if (property == null) + { + continue; + } + + var value = row[header]; + if (string.IsNullOrWhiteSpace(value)) + { + continue; + } + + var propertyType = property.PropertyType; + + //若是泛型且是集合 + if (propertyType.IsGenericType + && propertyType.GetGenericTypeDefinition() == typeof(List<>)) + { + var listType = propertyType.GetGenericArguments()[0]; + var list = JsonSerializer.Deserialize(value, typeof(List<>).MakeGenericType(listType)); + property.SetValue(instance, list); + } + + //若是物件且不是字串 + else if (propertyType.IsClass + && propertyType != typeof(string)) + { + var obj = JsonSerializer.Deserialize(value, propertyType); + property.SetValue(instance, obj); + } + + //若是列舉 + else if (propertyType.IsEnum) + { + property.SetValue(instance, Enum.Parse(propertyType, value)); + } + else + { + property.SetValue(instance, Convert.ChangeType(value, propertyType)); + } + } + + results.Add(instance); + } + + return results; + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowTips.sln b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowTips.sln new file mode 100644 index 00000000..1d9c31b8 --- /dev/null +++ b/Test/Specflow3/Lab.SpecflowTips/Lab.SpecflowTips.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.SpecflowCreateAndCompareJson", "Lab.SpecflowCreateAndCompareJson\Lab.SpecflowCreateAndCompareJson.csproj", "{692293AC-DA77-47AB-860F-341C7136C7FD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {692293AC-DA77-47AB-860F-341C7136C7FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {692293AC-DA77-47AB-860F-341C7136C7FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {692293AC-DA77-47AB-860F-341C7136C7FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {692293AC-DA77-47AB-860F-341C7136C7FD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo0.feature b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo0.feature new file mode 100644 index 00000000..4c80e487 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo0.feature @@ -0,0 +1,5 @@ +Feature: Demo0 + + Scenario: 測試步驟注入 + When 取得檔案路徑 + Then 預期得到 "File Provider" \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo0.feature.cs b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo0.feature.cs new file mode 100644 index 00000000..d52ddcbe --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo0.feature.cs @@ -0,0 +1,122 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (https://www.specflow.org/). +// SpecFlow Version:3.9.0.0 +// SpecFlow Generator Version:3.9.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code + +using TechTalk.SpecFlow; + +#pragma warning disable +namespace Lab.StepDependencyInjection.Test.Features +{ + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [TestClass()] + public partial class Demo0Feature + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private Microsoft.VisualStudio.TestTools.UnitTesting.TestContext _testContext; + + private static string[] featureTags = ((string[])(null)); + +#line 1 "Demo0.feature" +#line hidden + + public virtual Microsoft.VisualStudio.TestTools.UnitTesting.TestContext TestContext + { + get + { + return this._testContext; + } + set + { + this._testContext = value; + } + } + + [ClassInitialize()] + public static void FeatureSetup(Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext) + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Demo0", null, ProgrammingLanguage.CSharp, featureTags); + testRunner.OnFeatureStart(featureInfo); + } + + [ClassCleanup()] + public static void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + [TestInitialize()] + public void TestInitialize() + { + if (((testRunner.FeatureContext != null) + && (testRunner.FeatureContext.FeatureInfo.Title != "Demo0"))) + { + global::Lab.StepDependencyInjection.Test.Features.Demo0Feature.FeatureSetup(null); + } + } + + [TestCleanup()] + public void TestTearDown() + { + testRunner.OnScenarioEnd(); + } + + public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(this._testContext); + } + + public void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + [TestMethod()] + [Description("測試步驟注入")] + [TestProperty("FeatureTitle", "Demo0")] + public void 測試步驟注入() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("測試步驟注入", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 3 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + testRunner.When("取得檔案路徑", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 5 + testRunner.Then("預期得到 \"File Provider\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + } +} +#pragma warning restore +#endregion diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo1.feature b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo1.feature new file mode 100644 index 00000000..d2eeaff0 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo1.feature @@ -0,0 +1,5 @@ +Feature: Demo1 + + Scenario: 測試步驟注入 + When 取得檔案路徑 + Then 預期得到 "File Provider" diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo1.feature.cs b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo1.feature.cs new file mode 100644 index 00000000..16e71765 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo1.feature.cs @@ -0,0 +1,122 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (https://www.specflow.org/). +// SpecFlow Version:3.9.0.0 +// SpecFlow Generator Version:3.9.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code + +using TechTalk.SpecFlow; + +#pragma warning disable +namespace Lab.StepDependencyInjection.Test.Features +{ + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [TestClass()] + public partial class Demo1Feature + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private Microsoft.VisualStudio.TestTools.UnitTesting.TestContext _testContext; + + private static string[] featureTags = ((string[])(null)); + +#line 1 "Demo1.feature" +#line hidden + + public virtual Microsoft.VisualStudio.TestTools.UnitTesting.TestContext TestContext + { + get + { + return this._testContext; + } + set + { + this._testContext = value; + } + } + + [ClassInitialize()] + public static void FeatureSetup(Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext) + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Demo1", null, ProgrammingLanguage.CSharp, featureTags); + testRunner.OnFeatureStart(featureInfo); + } + + [ClassCleanup()] + public static void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + [TestInitialize()] + public void TestInitialize() + { + if (((testRunner.FeatureContext != null) + && (testRunner.FeatureContext.FeatureInfo.Title != "Demo1"))) + { + global::Lab.StepDependencyInjection.Test.Features.Demo1Feature.FeatureSetup(null); + } + } + + [TestCleanup()] + public void TestTearDown() + { + testRunner.OnScenarioEnd(); + } + + public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(this._testContext); + } + + public void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + [TestMethod()] + [Description("測試步驟注入")] + [TestProperty("FeatureTitle", "Demo1")] + public void 測試步驟注入() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("測試步驟注入", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 3 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + testRunner.When("取得檔案路徑", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 5 + testRunner.Then("預期得到 \"File Provider\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + } +} +#pragma warning restore +#endregion diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo2.feature b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo2.feature new file mode 100644 index 00000000..d7f0b85c --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo2.feature @@ -0,0 +1,5 @@ +Feature: Demo2 + + Scenario: 測試步驟注入 + When 取得檔案路徑 + Then 預期得到 "fake provider:SysFileProvider" diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo2.feature.cs b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo2.feature.cs new file mode 100644 index 00000000..8dbce9f2 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Features/Demo2.feature.cs @@ -0,0 +1,122 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (https://www.specflow.org/). +// SpecFlow Version:3.9.0.0 +// SpecFlow Generator Version:3.9.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code + +using TechTalk.SpecFlow; + +#pragma warning disable +namespace Lab.StepDependencyInjection.Test.Features +{ + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [TestClass()] + public partial class Demo2Feature + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private Microsoft.VisualStudio.TestTools.UnitTesting.TestContext _testContext; + + private static string[] featureTags = ((string[])(null)); + +#line 1 "Demo2.feature" +#line hidden + + public virtual Microsoft.VisualStudio.TestTools.UnitTesting.TestContext TestContext + { + get + { + return this._testContext; + } + set + { + this._testContext = value; + } + } + + [ClassInitialize()] + public static void FeatureSetup(Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext) + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Demo2", null, ProgrammingLanguage.CSharp, featureTags); + testRunner.OnFeatureStart(featureInfo); + } + + [ClassCleanup()] + public static void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + [TestInitialize()] + public void TestInitialize() + { + if (((testRunner.FeatureContext != null) + && (testRunner.FeatureContext.FeatureInfo.Title != "Demo2"))) + { + global::Lab.StepDependencyInjection.Test.Features.Demo2Feature.FeatureSetup(null); + } + } + + [TestCleanup()] + public void TestTearDown() + { + testRunner.OnScenarioEnd(); + } + + public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(this._testContext); + } + + public void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + [TestMethod()] + [Description("測試步驟注入")] + [TestProperty("FeatureTitle", "Demo2")] + public void 測試步驟注入() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("測試步驟注入", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 3 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + testRunner.When("取得檔案路徑", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 5 + testRunner.Then("預期得到 \"fake provider:SysFileProvider\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + } +} +#pragma warning restore +#endregion diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/FileProvider.cs b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/FileProvider.cs new file mode 100644 index 00000000..caead342 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/FileProvider.cs @@ -0,0 +1,9 @@ +namespace Lab.StepDependencyInjection.Test; + +public class FileProvider +{ + public string GetPath() + { + return "File Provider"; + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Lab.StepDependencyInjection.Test.csproj b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Lab.StepDependencyInjection.Test.csproj new file mode 100644 index 00000000..d3d8c81c --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Lab.StepDependencyInjection.Test.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Steps/Demo0Steps.cs b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Steps/Demo0Steps.cs new file mode 100644 index 00000000..775ba6b8 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Steps/Demo0Steps.cs @@ -0,0 +1,40 @@ +using TechTalk.SpecFlow; + +namespace Lab.StepDependencyInjection.Test.Steps; + +[Binding] +[Scope(Feature = "Demo0")] +public class Demo0Steps +{ + private readonly FileProvider _fileProvider; + + private ScenarioContext ScenarioContext { get; } + + public Demo0Steps(ScenarioContext scenarioContext, FileProvider fileProvider) + { + this.ScenarioContext = scenarioContext; + this._fileProvider = fileProvider; + } + + // [ScenarioDependencies] + // public static IServiceCollection CreateServices() + // { + // var services = new ServiceCollection(); + // services.AddSingleton(); + // return services; + // } + + [When(@"取得檔案路徑")] + public void When取得檔案路徑() + { + var path = this._fileProvider.GetPath(); + this.ScenarioContext.Set(path, "actual"); + } + + [Then(@"預期得到 ""(.*)""")] + public void Then預期得到(string expected) + { + var actual = this.ScenarioContext.Get("actual"); + Assert.AreEqual(expected, actual); + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Steps/Demo1Steps.cs b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Steps/Demo1Steps.cs new file mode 100644 index 00000000..aadd1733 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Steps/Demo1Steps.cs @@ -0,0 +1,36 @@ +using BoDi; +using TechTalk.SpecFlow; + +namespace Lab.StepDependencyInjection.Test.Steps; + +[Binding] +[Scope(Feature = "Demo1")] +public class Demo1Steps : TechTalk.SpecFlow.Steps +{ + private readonly IObjectContainer objectContainer; + + private ScenarioContext ScenarioContext { get; } + + public Demo1Steps(IObjectContainer objectContainer, + ScenarioContext scenarioContext) + { + objectContainer.RegisterInstanceAs(new FileProvider(), nameof(FileProvider)); + this.objectContainer = objectContainer; + this.ScenarioContext = scenarioContext; + } + + [When(@"取得檔案路徑")] + public void When取得檔案路徑() + { + var target = this.objectContainer.Resolve(nameof(FileProvider)); + var path = target.GetPath(); + this.ScenarioContext.Set(path, "actual"); + } + + [Then(@"預期得到 ""(.*)""")] + public void Then預期得到(string expected) + { + var actual = this.ScenarioContext.Get("actual"); + Assert.AreEqual(expected, actual); + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Steps/Demo2Steps.cs b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Steps/Demo2Steps.cs new file mode 100644 index 00000000..8f6307a8 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Steps/Demo2Steps.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.DependencyInjection; +using SolidToken.SpecFlow.DependencyInjection; +using TechTalk.SpecFlow; + +namespace Lab.StepDependencyInjection.Test.Steps; + +[Binding] +[Scope(Feature = "Demo2")] +public class Demo2Steps +{ + private readonly SysFileProvider _sysFileProvider; + + public Demo2Steps(SysFileProvider sysFileProvider, ScenarioContext scenarioContext) + { + this._sysFileProvider = sysFileProvider; + this.ScenarioContext = scenarioContext; + } + + private ScenarioContext ScenarioContext { get; } + + + [ScenarioDependencies] + public static IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(new SysFileProvider("fake provider")); + return services; + } + + [When(@"取得檔案路徑")] + public void When取得檔案路徑() + { + var path = this._sysFileProvider.GetPath(); + this.ScenarioContext.Set(path, "actual"); + } + + [Then(@"預期得到 ""(.*)""")] + public void Then預期得到(string expected) + { + var actual = this.ScenarioContext.Get("actual"); + Assert.AreEqual(expected, actual); + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/SysFileProvider.cs b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/SysFileProvider.cs new file mode 100644 index 00000000..b95858c1 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/SysFileProvider.cs @@ -0,0 +1,16 @@ +namespace Lab.StepDependencyInjection.Test; + +public class SysFileProvider +{ + private readonly string _name; + + public SysFileProvider(string name) + { + this._name = name; + } + + public string GetPath() + { + return $"{this._name}:SysFileProvider"; + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/UnitTest1.cs b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/UnitTest1.cs new file mode 100644 index 00000000..359bd5b8 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Lab.StepDependencyInjection.Test; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void TestMethod1() + { + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Usings.cs b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/specflow.json b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/specflow.json new file mode 100644 index 00000000..06565c4c --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.Test/specflow.json @@ -0,0 +1,10 @@ +{ + "language": { + "feature": "en-US" + }, + "stepAssemblies": [ + { + "assembly": "Allure.SpecFlowPlugin" + } + ] +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.sln b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.sln new file mode 100644 index 00000000..cd4d1aec --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjection/Lab.StepDependencyInjection.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.StepDependencyInjection.Test", "Lab.StepDependencyInjection.Test\Lab.StepDependencyInjection.Test.csproj", "{B44A3BAF-10A5-499A-B1D4-48F9BA333884}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B44A3BAF-10A5-499A-B1D4-48F9BA333884}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B44A3BAF-10A5-499A-B1D4-48F9BA333884}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B44A3BAF-10A5-499A-B1D4-48F9BA333884}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B44A3BAF-10A5-499A-B1D4-48F9BA333884}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo0.feature b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo0.feature new file mode 100644 index 00000000..23b60576 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo0.feature @@ -0,0 +1,5 @@ +Feature: Demo0 + + Scenario: 測試步驟注入 + When 寫入資料表 + When 讀取資料表 \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo0.feature.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo0.feature.cs new file mode 100644 index 00000000..b8d63808 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo0.feature.cs @@ -0,0 +1,124 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (https://www.specflow.org/). +// SpecFlow Version:3.9.0.0 +// SpecFlow Generator Version:3.9.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code +#pragma warning disable +namespace Lab.StepDependencyInjection.Test.Features +{ + using TechTalk.SpecFlow; + using System; + using System.Linq; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute()] + public partial class Demo0Feature + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private Microsoft.VisualStudio.TestTools.UnitTesting.TestContext _testContext; + + private static string[] featureTags = ((string[])(null)); + +#line 1 "Demo0.feature" +#line hidden + + public virtual Microsoft.VisualStudio.TestTools.UnitTesting.TestContext TestContext + { + get + { + return this._testContext; + } + set + { + this._testContext = value; + } + } + + [Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitializeAttribute()] + public static void FeatureSetup(Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext) + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Demo0", null, ProgrammingLanguage.CSharp, featureTags); + testRunner.OnFeatureStart(featureInfo); + } + + [Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanupAttribute()] + public static void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + [Microsoft.VisualStudio.TestTools.UnitTesting.TestInitializeAttribute()] + public void TestInitialize() + { + if (((testRunner.FeatureContext != null) + && (testRunner.FeatureContext.FeatureInfo.Title != "Demo0"))) + { + global::Lab.StepDependencyInjection.Test.Features.Demo0Feature.FeatureSetup(null); + } + } + + [Microsoft.VisualStudio.TestTools.UnitTesting.TestCleanupAttribute()] + public void TestTearDown() + { + testRunner.OnScenarioEnd(); + } + + public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(_testContext); + } + + public void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute()] + [Microsoft.VisualStudio.TestTools.UnitTesting.DescriptionAttribute("測試步驟注入")] + [Microsoft.VisualStudio.TestTools.UnitTesting.TestPropertyAttribute("FeatureTitle", "Demo0")] + public void 測試步驟注入() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("測試步驟注入", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 3 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + testRunner.When("寫入資料表", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 5 + testRunner.When("讀取資料表", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden + } + this.ScenarioCleanup(); + } + } +} +#pragma warning restore +#endregion diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo1.feature b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo1.feature new file mode 100644 index 00000000..9dde1711 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo1.feature @@ -0,0 +1,4 @@ +Feature: Demo1 + + Scenario: 測試步驟注入 + When 呼叫 Web API \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo1.feature.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo1.feature.cs new file mode 100644 index 00000000..54cc09bc --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Features/Demo1.feature.cs @@ -0,0 +1,121 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (https://www.specflow.org/). +// SpecFlow Version:3.9.0.0 +// SpecFlow Generator Version:3.9.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code +#pragma warning disable +namespace Lab.StepDependencyInjection.Test.Features +{ + using TechTalk.SpecFlow; + using System; + using System.Linq; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute()] + public partial class Demo1Feature + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private Microsoft.VisualStudio.TestTools.UnitTesting.TestContext _testContext; + + private static string[] featureTags = ((string[])(null)); + +#line 1 "Demo1.feature" +#line hidden + + public virtual Microsoft.VisualStudio.TestTools.UnitTesting.TestContext TestContext + { + get + { + return this._testContext; + } + set + { + this._testContext = value; + } + } + + [Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitializeAttribute()] + public static void FeatureSetup(Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext) + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Demo1", null, ProgrammingLanguage.CSharp, featureTags); + testRunner.OnFeatureStart(featureInfo); + } + + [Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanupAttribute()] + public static void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + [Microsoft.VisualStudio.TestTools.UnitTesting.TestInitializeAttribute()] + public void TestInitialize() + { + if (((testRunner.FeatureContext != null) + && (testRunner.FeatureContext.FeatureInfo.Title != "Demo1"))) + { + global::Lab.StepDependencyInjection.Test.Features.Demo1Feature.FeatureSetup(null); + } + } + + [Microsoft.VisualStudio.TestTools.UnitTesting.TestCleanupAttribute()] + public void TestTearDown() + { + testRunner.OnScenarioEnd(); + } + + public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(_testContext); + } + + public void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute()] + [Microsoft.VisualStudio.TestTools.UnitTesting.DescriptionAttribute("測試步驟注入")] + [Microsoft.VisualStudio.TestTools.UnitTesting.TestPropertyAttribute("FeatureTitle", "Demo1")] + public void 測試步驟注入() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("測試步驟注入", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 3 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + testRunner.When("呼叫 Web API", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden + } + this.ScenarioCleanup(); + } + } +} +#pragma warning restore +#endregion diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/FileProvider.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/FileProvider.cs new file mode 100644 index 00000000..caead342 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/FileProvider.cs @@ -0,0 +1,9 @@ +namespace Lab.StepDependencyInjection.Test; + +public class FileProvider +{ + public string GetPath() + { + return "File Provider"; + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/GlobalSteps.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/GlobalSteps.cs new file mode 100644 index 00000000..0d1884a4 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/GlobalSteps.cs @@ -0,0 +1,32 @@ +using Lab.StepDependencyInjection.WebAPI.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using TechTalk.SpecFlow; + +namespace Lab.StepDependencyInjection.Test; + +[Binding] +public class GlobalSteps +{ + internal static ServiceProvider ServiceProvider; + + [BeforeTestRun] + public static void BeforeTestRun() + { + var service = InstanceManager.InitializeServices(); + ServiceProvider = service.BuildServiceProvider(); + var factory = ServiceProvider.GetService>(); + using var dbContext = factory.CreateDbContext(); + dbContext.Database.EnsureDeleted(); + dbContext.Database.EnsureCreated(); + } + + [AfterTestRun] + public static void AfterTestRun() + { + var factory = ServiceProvider.GetService>(); + using var dbContext = factory.CreateDbContext(); + dbContext.Database.EnsureDeleted(); + dbContext.Database.EnsureCreated(); + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/InstanceManager.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/InstanceManager.cs new file mode 100644 index 00000000..8759d833 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/InstanceManager.cs @@ -0,0 +1,42 @@ +using Lab.StepDependencyInjection.WebAPI.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.StepDependencyInjection.Test; + +internal class InstanceManager +{ + public static IServiceCollection InitializeServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + + services.AddLogging(p => p.AddConsole()); + services.AddDbContextFactory((p, options) => + { + var loggerFactory = p.GetService(); + var logger = loggerFactory.CreateLogger("測試"); + logger.LogInformation("選用測試專案的的注入設定"); + var connectionString = + "Host=localhost;Port=5432;Database=employee_test;Username=postgres;Password=guest;"; + + // var connectionString = sp.GetService().Value; + options.UseNpgsql(connectionString, //只會呼叫一次 + builder => builder.EnableRetryOnFailure( + 10, + TimeSpan.FromSeconds(30), + new List { "57P01" }) + ) + + // .UseLazyLoadingProxies() + .UseSnakeCaseNamingConvention() + .EnableSensitiveDataLogging() + .UseLoggerFactory(loggerFactory) + ; + + //.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + }, lifetime: ServiceLifetime.Transient); + return services; + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Lab.StepDependencyInjection.Test.csproj b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Lab.StepDependencyInjection.Test.csproj new file mode 100644 index 00000000..f570fb3f --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Lab.StepDependencyInjection.Test.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + + + + diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Steps/Demo0Steps.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Steps/Demo0Steps.cs new file mode 100644 index 00000000..e1f80317 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Steps/Demo0Steps.cs @@ -0,0 +1,59 @@ +using Lab.StepDependencyInjection.WebAPI.EntityModel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SolidToken.SpecFlow.DependencyInjection; +using TechTalk.SpecFlow; + +namespace Lab.StepDependencyInjection.Test.Steps; + +[Binding] +[Scope(Feature = "Demo0")] +public class Demo0Steps +{ + private readonly FileProvider _fileProvider; + + private ScenarioContext ScenarioContext { get; } + + private IDbContextFactory _dbContextFactory; + + public Demo0Steps(ScenarioContext scenarioContext, + FileProvider fileProvider, + IDbContextFactory dbContextFactory) + { + this.ScenarioContext = scenarioContext; + this._fileProvider = fileProvider; + this._dbContextFactory = dbContextFactory; + } + + [ScenarioDependencies] + public static IServiceCollection CreateServices() + { + return InstanceManager.InitializeServices(); + } + + [When(@"寫入資料表")] + public void When寫入資料表() + { + // var dbContextFactory = GlobalSteps.ServiceProvider.GetService>(); + var dbContextFactory = this._dbContextFactory; + using var dbContext = dbContextFactory.CreateDbContext(); + dbContext.Add(new Employee + { + Id = Guid.NewGuid(), + Name = "yao", + Age = 19, + CreateAt = DateTime.UtcNow, + CreateBy = "yao" + }); + dbContext.SaveChanges(); + } + + [When(@"讀取資料表")] + public void When讀取資料表() + { + // var dbContextFactory = GlobalSteps.ServiceProvider.GetService>(); + var dbContextFactory = this._dbContextFactory; + using var dbContext = dbContextFactory.CreateDbContext(); + var employees = dbContext.Employees.AsNoTracking().ToList(); + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Steps/Demo1Steps.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Steps/Demo1Steps.cs new file mode 100644 index 00000000..8a9f895e --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Steps/Demo1Steps.cs @@ -0,0 +1,16 @@ +using TechTalk.SpecFlow; + +namespace Lab.StepDependencyInjection.Test.Steps; + +[Binding] +public class Demo1Steps +{ + [When(@"呼叫 Web API")] + public async Task When呼叫WebApi() + { + var testServer = new TestServer(); + var client = testServer.CreateClient(); + var response = await client.GetAsync("Default"); + var response1 = await client.GetAsync("Demo"); + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/SysFileProvider.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/SysFileProvider.cs new file mode 100644 index 00000000..b95858c1 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/SysFileProvider.cs @@ -0,0 +1,16 @@ +namespace Lab.StepDependencyInjection.Test; + +public class SysFileProvider +{ + private readonly string _name; + + public SysFileProvider(string name) + { + this._name = name; + } + + public string GetPath() + { + return $"{this._name}:SysFileProvider"; + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/TestServer.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/TestServer.cs new file mode 100644 index 00000000..a995a168 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/TestServer.cs @@ -0,0 +1,21 @@ +using Lab.StepDependencyInjection.WebAPI; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Lab.StepDependencyInjection.Test; + +public class TestServer : WebApplicationFactory +{ + private void ConfigureServices(IServiceCollection services) + { + services.AddLogging(p => p.AddConsole()); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(this.ConfigureServices); + builder.UseSetting("https_port", "9527"); + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/UnitTest1.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/UnitTest1.cs new file mode 100644 index 00000000..359bd5b8 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Lab.StepDependencyInjection.Test; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void TestMethod1() + { + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Usings.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/specflow.json b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/specflow.json new file mode 100644 index 00000000..06565c4c --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.Test/specflow.json @@ -0,0 +1,10 @@ +{ + "language": { + "feature": "en-US" + }, + "stepAssemblies": [ + { + "assembly": "Allure.SpecFlowPlugin" + } + ] +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/AppSettings.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/AppSettings.cs new file mode 100644 index 00000000..07a7461b --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/AppSettings.cs @@ -0,0 +1,6 @@ +namespace Lab.StepDependencyInjection.WebAPI; + +public class AppSettings +{ + public string ConnectionString { get; init; } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Controllers/DefaultController.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Controllers/DefaultController.cs new file mode 100644 index 00000000..fa82210c --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Controllers/DefaultController.cs @@ -0,0 +1,26 @@ +using Lab.StepDependencyInjection.WebAPI.EntityModel; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Lab.StepDependencyInjection.WebAPI.Controllers; + +[ApiController] +[Route("[controller]")] +public class DefaultController : ControllerBase +{ + private readonly IDbContextFactory _employeeDbContextFactory; + + public DefaultController(IDbContextFactory employeeDbContextFactory) + { + this._employeeDbContextFactory = employeeDbContextFactory; + } + + [HttpGet()] + public async Task Get() + { + await using var dbContext = await this._employeeDbContextFactory.CreateDbContextAsync(); + await dbContext.Database.EnsureCreatedAsync(); + var data = await dbContext.Employees.Where(p => p.Age == 10).AsNoTracking().ToListAsync(); + return this.Ok(data); + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/EntityModel/Employee.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/EntityModel/Employee.cs new file mode 100644 index 00000000..99c9115e --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/EntityModel/Employee.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Lab.StepDependencyInjection.WebAPI.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; } + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/EntityModel/EmployeeDbContext.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/EntityModel/EmployeeDbContext.cs new file mode 100644 index 00000000..8df2c3fa --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/EntityModel/EmployeeDbContext.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; + +namespace Lab.StepDependencyInjection.WebAPI.EntityModel +{ + public class EmployeeDbContext : DbContext + { + public virtual DbSet Employees { get; set; } + + public EmployeeDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(p => + { + p.HasKey(e => e.Id); + p.HasIndex(e => e.SequenceId) + .IsUnique(); + p.Property(p => p.Remark).IsRequired(false); + }); + } + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/EntityModel/EmployeeDbContextContextFactory.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/EntityModel/EmployeeDbContextContextFactory.cs new file mode 100644 index 00000000..4075e8c6 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/EntityModel/EmployeeDbContextContextFactory.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Lab.StepDependencyInjection.WebAPI.EntityModel; + +public class EmployeeDbContextContextFactory : IDesignTimeDbContextFactory +{ + public EmployeeDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder + .UseNpgsql("Host=localhost;Port=5432;Database=employee;Username=postgres;Password=guest") + .UseSnakeCaseNamingConvention(); + + return new EmployeeDbContext(optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Lab.StepDependencyInjection.WebAPI.csproj b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Lab.StepDependencyInjection.WebAPI.csproj new file mode 100644 index 00000000..19c25f17 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Lab.StepDependencyInjection.WebAPI.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + enable + enable + + + + + + + + + + diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Program.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Program.cs new file mode 100644 index 00000000..bedf26bc --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Program.cs @@ -0,0 +1,31 @@ +using Lab.StepDependencyInjection.WebAPI; +using Lab.StepDependencyInjection.WebAPI.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.ConfigureServices(); +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(); +public partial class Program { } diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Properties/launchSettings.json b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Properties/launchSettings.json new file mode 100644 index 00000000..d33a047d --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:58129", + "sslPort": 44362 + } + }, + "profiles": { + "Lab.StepDependencyInjection.WebAPI": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7013;http://localhost:5088", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/ServiceCollectionExtensions.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..e02cabb8 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/ServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Lab.StepDependencyInjection.WebAPI.EntityModel; +using Microsoft.EntityFrameworkCore; + +namespace Lab.StepDependencyInjection.WebAPI; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection ConfigureServices(this IServiceCollection services) + { + services.AddSingleton(p => new AppSettings + { + ConnectionString = "Host=localhost;Port=5432;Database=employee;Username=postgres;Password=guest" + }); + services.AddDbContextFactory((provider, builder) => + { + var appSettings = provider.GetService(); + var connectionString = appSettings.ConnectionString; + builder.UseNpgsql(connectionString); + }); + return services; + } +} \ No newline at end of file diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/WeatherForecast.cs b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/WeatherForecast.cs new file mode 100644 index 00000000..1635976b --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Lab.StepDependencyInjection.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/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/appsettings.Development.json b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/appsettings.json b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjection.WebAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjectionAndDbContext.sln b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjectionAndDbContext.sln new file mode 100644 index 00000000..1edbba98 --- /dev/null +++ b/Test/Specflow3/Lab.StepDependencyInjectionAndDbContext/Lab.StepDependencyInjectionAndDbContext.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.StepDependencyInjection.Test", "Lab.StepDependencyInjection.Test\Lab.StepDependencyInjection.Test.csproj", "{B44A3BAF-10A5-499A-B1D4-48F9BA333884}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.StepDependencyInjection.WebAPI", "Lab.StepDependencyInjection.WebAPI\Lab.StepDependencyInjection.WebAPI.csproj", "{794E332B-0A00-4FFB-A951-877B8DD4B927}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B44A3BAF-10A5-499A-B1D4-48F9BA333884}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B44A3BAF-10A5-499A-B1D4-48F9BA333884}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B44A3BAF-10A5-499A-B1D4-48F9BA333884}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B44A3BAF-10A5-499A-B1D4-48F9BA333884}.Release|Any CPU.Build.0 = Release|Any CPU + {794E332B-0A00-4FFB-A951-877B8DD4B927}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {794E332B-0A00-4FFB-A951-877B8DD4B927}.Debug|Any CPU.Build.0 = Debug|Any CPU + {794E332B-0A00-4FFB-A951-877B8DD4B927}.Release|Any CPU.ActiveCfg = Release|Any CPU + {794E332B-0A00-4FFB-A951-877B8DD4B927}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Test/nbomber/Lab.NBomberTest/.gitignore b/Test/nbomber/Lab.NBomberTest/.gitignore new file mode 100644 index 00000000..a33719ce --- /dev/null +++ b/Test/nbomber/Lab.NBomberTest/.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/nbomber/Lab.NBomberTest/Lab.NBomberTest.sln b/Test/nbomber/Lab.NBomberTest/Lab.NBomberTest.sln new file mode 100644 index 00000000..fac4faae --- /dev/null +++ b/Test/nbomber/Lab.NBomberTest/Lab.NBomberTest.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{029D0395-F267-4B37-BFD7-D6CC61D0495B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.NBomberTest", "test\Lab.NBomberTest\Lab.NBomberTest.csproj", "{90320F28-4CD0-4A7A-A95E-9F1D642EFF74}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7C968F1D-D31D-4B7E-93AB-0D13DDA9AA34}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.NBomberTest.App", "src\Lab.NBomberTest.App\Lab.NBomberTest.App.csproj", "{7EF73F64-28FF-4F46-8331-431BDA6AC3EF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {90320F28-4CD0-4A7A-A95E-9F1D642EFF74} = {029D0395-F267-4B37-BFD7-D6CC61D0495B} + {7EF73F64-28FF-4F46-8331-431BDA6AC3EF} = {7C968F1D-D31D-4B7E-93AB-0D13DDA9AA34} + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {90320F28-4CD0-4A7A-A95E-9F1D642EFF74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90320F28-4CD0-4A7A-A95E-9F1D642EFF74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90320F28-4CD0-4A7A-A95E-9F1D642EFF74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90320F28-4CD0-4A7A-A95E-9F1D642EFF74}.Release|Any CPU.Build.0 = Release|Any CPU + {7EF73F64-28FF-4F46-8331-431BDA6AC3EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EF73F64-28FF-4F46-8331-431BDA6AC3EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EF73F64-28FF-4F46-8331-431BDA6AC3EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EF73F64-28FF-4F46-8331-431BDA6AC3EF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Test/nbomber/Lab.NBomberTest/src/Lab.NBomberTest.App/Lab.NBomberTest.App.csproj b/Test/nbomber/Lab.NBomberTest/src/Lab.NBomberTest.App/Lab.NBomberTest.App.csproj new file mode 100644 index 00000000..91fd3274 --- /dev/null +++ b/Test/nbomber/Lab.NBomberTest/src/Lab.NBomberTest.App/Lab.NBomberTest.App.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/Test/nbomber/Lab.NBomberTest/src/Lab.NBomberTest.App/Program.cs b/Test/nbomber/Lab.NBomberTest/src/Lab.NBomberTest.App/Program.cs new file mode 100644 index 00000000..5fc4623a --- /dev/null +++ b/Test/nbomber/Lab.NBomberTest/src/Lab.NBomberTest.App/Program.cs @@ -0,0 +1,32 @@ +using NBomber.Contracts; +using NBomber.CSharp; +using NBomber.Plugins.Http.CSharp; + +var httpFactory = ClientFactory.Create( + name: "http_factory", + clientCount: 1, + initClient: (number,context) => Task.FromResult(new HttpClient()) +); + +var step1 = Step.Create("1", + clientFactory: HttpClientFactory.Create("1"), + execute: async context => + { + var response = await context.Client.GetAsync("http://test.k6.io", context.CancellationToken); + + return response.IsSuccessStatusCode + ? Response.Ok(statusCode: (int)response.StatusCode) + : Response.Fail(statusCode: (int)response.StatusCode); + }); +var scenario1 = ScenarioBuilder.CreateScenario("1", step1) + .WithLoadSimulations(Simulation.InjectPerSec(rate: 10, during: TimeSpan.FromSeconds(30))); + + +// var pingPluginConfig = PingPluginConfig.CreateDefault(new[] { "test.k6.io" }); +// var pingPlugin = new PingPlugin(pingPluginConfig); +// +NBomberRunner + .RegisterScenarios(scenario1) + + // .WithWorkerPlugins(pingPlugin) + .Run(); \ No newline at end of file diff --git a/Test/nbomber/Lab.NBomberTest/src/Lab.NBomberTest.App/Program1.cs b/Test/nbomber/Lab.NBomberTest/src/Lab.NBomberTest.App/Program1.cs new file mode 100644 index 00000000..5bf4b6b1 --- /dev/null +++ b/Test/nbomber/Lab.NBomberTest/src/Lab.NBomberTest.App/Program1.cs @@ -0,0 +1,26 @@ +// using NBomber.Contracts; +// using NBomber.CSharp; +// using NBomber.Plugins.Http.CSharp; +// +// var httpFactory = HttpClientFactory.Create(); +// +// var step = Step.Create("fetch_html_page", +// clientFactory: httpFactory, +// execute: async context => +// { +// var response = await context.Client.GetAsync("https://test.k6.io/", context.CancellationToken); +// +// return response.IsSuccessStatusCode +// ? Response.Ok(statusCode: (int)response.StatusCode) +// : Response.Fail(statusCode: (int)response.StatusCode); +// }); +// +// var scenario = ScenarioBuilder +// .CreateScenario("simple_http", step) +// .WithWarmUpDuration(TimeSpan.FromSeconds(5)) +// .WithLoadSimulations(new[] +// { +// Simulation.InjectPerSec(rate: 100, during: TimeSpan.FromSeconds(30)) +// }); +// +// NBomberRunner.RegisterScenarios(scenario).Run(); \ No newline at end of file diff --git a/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/Lab.NBomberTest.csproj b/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/Lab.NBomberTest.csproj new file mode 100644 index 00000000..bea799f2 --- /dev/null +++ b/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/Lab.NBomberTest.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + diff --git a/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/Usings.cs b/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/features/test.feature b/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/features/test.feature new file mode 100644 index 00000000..65af3d29 --- /dev/null +++ b/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/features/test.feature @@ -0,0 +1,8 @@ +Feature: test + + Scenario: 壓力測試 + Given 準備以下 Header 參數 + | Key | Value | + | x-api-key | 123456 | + Given 準備 HttpRequest 'GET', "http://test.k6.io" + Then 執行測試 \ No newline at end of file diff --git a/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/steps/test.cs b/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/steps/test.cs new file mode 100644 index 00000000..18cb6564 --- /dev/null +++ b/Test/nbomber/Lab.NBomberTest/test/Lab.NBomberTest/steps/test.cs @@ -0,0 +1,64 @@ +using NBomber.Contracts; +using NBomber.CSharp; +using NBomber.Plugins.Http.CSharp; +using TechTalk.SpecFlow; + +namespace Lab.NBomberTest.steps; + +[Binding] +public class test : Steps +{ + [Given(@"準備以下 Header 參數")] + public void Given準備以下Header參數(Table table) + { + var headers = new Dictionary(); + foreach (var row in table.Rows) + { + if (headers.ContainsKey(row["Key"]) == false) + { + headers.Add("key", row["Value"]); + } + } + + this.ScenarioContext.Set(headers, "headers"); + } + + [Given(@"準備 HttpRequest '(.*)', ""(.*)""")] + public void Given準備HttpRequest(string httpMethod, string url) + { + var httpFactory = HttpClientFactory.Create(); + + this.ScenarioContext.TryGetValue>("headers", out var headers); + var step = Step.Create($"{httpMethod}-{url}", + httpFactory, + async context => + { + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in headers) + { + httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + // await context.Client.SendAsync(httpRequestMessage); + var response = await context.Client.GetAsync(url, context.CancellationToken); + + return response.IsSuccessStatusCode + ? Response.Ok(statusCode: (int)response.StatusCode) + : Response.Fail(statusCode: (int)response.StatusCode); + }); + + this.ScenarioContext.Set(step, "step"); + } + + [Then(@"執行測試")] + public void Then執行測試() + { + var step = this.ScenarioContext.Get("step"); + var scenario = ScenarioBuilder.CreateScenario("demo", step) + .WithLoadSimulations(Simulation.InjectPerSec(30, TimeSpan.FromSeconds(60))) + ; + var result = NBomberRunner + .RegisterScenarios(scenario) + .Run(); + } +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.ConsoleApp/Lab.Context.Trace.ConsoleApp.csproj b/Trace/Lab.Context.Trace/Lab.Context.Trace.ConsoleApp/Lab.Context.Trace.ConsoleApp.csproj new file mode 100644 index 00000000..2b14c817 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.ConsoleApp/Lab.Context.Trace.ConsoleApp.csproj @@ -0,0 +1,10 @@ + + + + Exe + net7.0 + enable + enable + + + diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.ConsoleApp/Program.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace.ConsoleApp/Program.cs new file mode 100644 index 00000000..9df00e03 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.ConsoleApp/Program.cs @@ -0,0 +1,69 @@ +// See https://aka.ms/new-console-template for more information + +using System.Diagnostics; + +Console.WriteLine("Starting request..."); + +using (var httpClient = new HttpClient()) +{ + var url = $"https://localhost:7004/demo"; + + var tasks = new List>(); + for (var i = 0; i < 2; i++) + { + tasks.Add(SendAsync(httpClient, url)); + } + + var data = await Task.WhenAll(tasks); + + // 列出重複的 trace id + var duplicateData = data.GroupBy(p => p?.TraceId) + .Where(p => p?.Count() > 1) + .Where(p => string.IsNullOrWhiteSpace(p?.Key) == false) + .Select(p => p?.Key); + + var items = new List(); + + foreach (var item in duplicateData) + { + if (string.IsNullOrWhiteSpace(item)) + { + continue; + } + + items.Add(item); + Console.WriteLine(item); + } + + if (items.Any()) + { + Debug.Fail("有重複的 trace id"); + } + + Console.WriteLine("All requests completed."); +} + +static async Task SendAsync(HttpClient httpClient, string url) +{ + try + { + var response = await httpClient.GetAsync(url); + response.Headers.TryGetValues("x-trace-id", out var traceIds); + var traceId = traceIds.FirstOrDefault(); + return new Data() + { + TraceId = traceId + }; + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + return null; +} + +class Data +{ + public string TraceId { get; set; } +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/Lab.Context.Trace.WebAPI.TestProject.csproj b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/Lab.Context.Trace.WebAPI.TestProject.csproj new file mode 100644 index 00000000..232b89c7 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/Lab.Context.Trace.WebAPI.TestProject.csproj @@ -0,0 +1,23 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + + + + + + + diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/TestServer.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/TestServer.cs new file mode 100644 index 00000000..be937d60 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/TestServer.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestPlatform.TestHost; + +namespace Lab.Context.Trace.WebAPI.TestProject; + +public class TestServer : WebApplicationFactory +{ + private void ConfigureServices(IServiceCollection services) + { + + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(this.ConfigureServices); + } +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/UnitTest1.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/UnitTest1.cs new file mode 100644 index 00000000..642c4bbc --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/UnitTest1.cs @@ -0,0 +1,52 @@ +namespace Lab.Context.Trace.WebAPI.TestProject; + +class Data +{ + public string? TraceId { get; set; } +} + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public async Task TestMethod1() + { + var server = new TestServer(); + var httpClient = server.CreateDefaultClient(); + + var url = "https://localhost:7004/demo"; + + var tasks = new List>(); + for (var i = 0; i < 10000; i++) + { + tasks.Add(SendAsync(httpClient, url)); + } + + var data = await Task.WhenAll(tasks); + + var duplicateData = data.GroupBy(p => p.TraceId) + .Where(p => p.Count() > 1) + .Select(p => p.Key); + + foreach (var item in duplicateData) + { + Console.WriteLine(item); + } + + if (duplicateData.Any()) + { + Assert.Fail("有重複的 trace id"); + } + } + + static async Task SendAsync(HttpClient httpClient, string url) + { + var response = await httpClient.GetAsync(url); + response.Headers.TryGetValues("x-trace-id", out var traceIds); + var traceId = traceIds.FirstOrDefault(); + return new Data() + { + TraceId = traceId + }; + } +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/Usings.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI.TestProject/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Controllers/DemoController.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Controllers/DemoController.cs new file mode 100644 index 00000000..0beaf5d4 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Controllers/DemoController.cs @@ -0,0 +1,32 @@ +using Lab.Context.Trace.WebAPI.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.Context.Trace.WebAPI.Controllers; + +[ApiController] +[Route("[controller]")] +public class DemoController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IContextGetter _authContextGetter; + + public DemoController(ILogger logger, + IContextGetter authContextGetter) + { + this._logger = logger; + this._authContextGetter = authContextGetter; + } + + [HttpGet(Name = "GetDemo")] + public ActionResult Get() + { + var authContext = this._authContextGetter.Get(); + var userId = authContext.UserId; + // 由 Context 取得 UserId + var member = Member.GetFakeMembers().FirstOrDefault(p => p.UserId == userId); + + this._logger.LogInformation(2000, "found {@Data}", member); + + return this.Ok(member); + } +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Lab.Context.Trace.WebAPI.csproj b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Lab.Context.Trace.WebAPI.csproj new file mode 100644 index 00000000..7aba19cd --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Lab.Context.Trace.WebAPI.csproj @@ -0,0 +1,24 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Models/Failure.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Models/Failure.cs new file mode 100644 index 00000000..17452f1e --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Models/Failure.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace Lab.Context.Trace.WebAPI.Models; + +public class Failure +{ + public Failure() + { + } + + public Failure(FailureCode code, string message) + { + this.Code = code; + this.Message = message; + } + + /// + /// 錯誤碼 + /// + public FailureCode Code { get; init; } + + /// + /// 錯誤訊息 + /// + public string Message { get; init; } + + /// + /// 錯誤發生時的資料 + /// + public object Data { get; init; } + + /// + /// 追蹤 Id + /// + public string TraceId { get; set; } + + /// + /// 例外,不回傳給 Web API + /// + [JsonIgnore] + public Exception Exception { get; set; } + + public List Details { get; init; } = new(); +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Models/FailureCode.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Models/FailureCode.cs new file mode 100644 index 00000000..4eec0439 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Models/FailureCode.cs @@ -0,0 +1,6 @@ +namespace Lab.Context.Trace.WebAPI.Models; + +public enum FailureCode +{ + Unauthorized +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Models/Member.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Models/Member.cs new file mode 100644 index 00000000..2c2f7896 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Models/Member.cs @@ -0,0 +1,30 @@ +namespace Lab.Context.Trace.WebAPI.Models; + +internal class Member +{ + public string UserId { get; set; } + + public int Age { get; set; } + + public string Name { get; set; } + + public static IEnumerable GetFakeMembers() + { + return new List() + { + new() + { + UserId = "yao", + Age = 19, + Name = "小章" + }, + new() + { + UserId = "yao1", + Age = 21, + Name = "小章1" + }, + + }; + } +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Program.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Program.cs new file mode 100644 index 00000000..e5704161 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Program.cs @@ -0,0 +1,71 @@ +using Lab.Context.Trace; +using Lab.Context.Trace.WebAPI; +using Serilog; +using Serilog.Events; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Hour) + .CreateLogger(); +Log.Information("Starting web host"); + +try +{ + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + builder.Services.AddControllers(); + + 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) + ); + + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + // builder.Services.AddScoped>(); + // builder.Services.AddScoped>(p => p.GetService>()); + // builder.Services.AddScoped>(p => p.GetService>()); + + builder.Services.AddSingleton>(); + builder.Services.AddSingleton>(p => p.GetService>()); + builder.Services.AddSingleton>(p => p.GetService>()); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseSerilogRequestLogging(); + app.UseHttpsRedirection(); + + app.UseAuthorization(); + app.UseMiddleware(); + app.MapControllers(); + app.Run(); + return 0; +} +catch (Exception ex) +{ + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} + +public partial class Program { } diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Properties/launchSettings.json b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Properties/launchSettings.json new file mode 100644 index 00000000..6807ef37 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:40069", + "sslPort": 44377 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5294", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7004;http://localhost:5294", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/SysHeaderNames.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/SysHeaderNames.cs new file mode 100644 index 00000000..f63f83a2 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/SysHeaderNames.cs @@ -0,0 +1,6 @@ +namespace Lab.Context.Trace.WebAPI; + +public abstract class SysHeaderNames +{ + public const string TraceId = "x-trace-id"; +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/TraceContextMiddleware.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/TraceContextMiddleware.cs new file mode 100644 index 00000000..144f22a1 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/TraceContextMiddleware.cs @@ -0,0 +1,76 @@ +using System.Security.Claims; +using Lab.Context.Trace.WebAPI.Models; + +namespace Lab.Context.Trace.WebAPI; + +public class TraceContextMiddleware +{ + private readonly RequestDelegate _next; + + public TraceContextMiddleware(RequestDelegate next) + { + this._next = next; + } + + public async Task Invoke(HttpContext httpContext, ILogger logger) + { + var traceId = httpContext.Request.Headers[SysHeaderNames.TraceId].FirstOrDefault(); + + //// 若調用端沒有傳入 traceId,則產生一個新的 traceId + if (string.IsNullOrWhiteSpace(traceId)) + { + traceId = httpContext.TraceIdentifier; + } + + // 模擬登入 + Signin(httpContext); + + if (httpContext.User.Identity.IsAuthenticated == false) + { + httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; + await httpContext.Response.WriteAsJsonAsync(new Failure + { + Code = FailureCode.Unauthorized, + Message = "not login", + }); + return; + } + + var userId = httpContext.User.Identity.Name; + + // 寫入 trace context 到 object context setter + var authContextSetter = httpContext.RequestServices.GetService>(); + authContextSetter.Set(new AuthContext + { + TraceId = traceId, + UserId = userId + }); + + // 附加 traceId 與 userId 到 log 中 + using var _ = logger.BeginScope("{Location},{TraceId},{UserId}", + "TW", traceId, userId); + + // 附加 traceId 到 response header 中 + IContextGetter? contextGetter = httpContext.RequestServices.GetService>(); + var traceContext = contextGetter.Get(); + httpContext.Response.Headers.TryAdd(SysHeaderNames.TraceId, traceContext.TraceId); + + await this._next.Invoke(httpContext); + } + + /// + /// 假的登入 + /// + /// + private static void Signin(HttpContext context) + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "yao"), + new Claim(ClaimTypes.Name, "yao"), + }; + var identity = new ClaimsIdentity(claims, "Bearer"); + var principal = new ClaimsPrincipal(identity); + context.User = principal; + } +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/appsettings.Development.json b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/appsettings.json b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.WebAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace.sln b/Trace/Lab.Context.Trace/Lab.Context.Trace.sln new file mode 100644 index 00000000..d91ea657 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Context.Trace", "Lab.Context.Trace\Lab.Context.Trace.csproj", "{85EF9A0D-F431-4C29-9FDD-E39C38A3D027}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Context.Trace.WebAPI", "Lab.Context.Trace.WebAPI\Lab.Context.Trace.WebAPI.csproj", "{04987A3C-032F-49CB-B59A-80D188BF1372}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{47606710-3952-41C4-8B80-57FB42EF000C}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Context.Trace.WebAPI.TestProject", "Lab.Context.Trace.WebAPI.TestProject\Lab.Context.Trace.WebAPI.TestProject.csproj", "{072B154D-149F-416C-AC1A-E009FED7706E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Context.Trace.ConsoleApp", "Lab.Context.Trace.ConsoleApp\Lab.Context.Trace.ConsoleApp.csproj", "{395822D5-7405-41B8-AC00-9106BF00EB76}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {85EF9A0D-F431-4C29-9FDD-E39C38A3D027}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85EF9A0D-F431-4C29-9FDD-E39C38A3D027}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85EF9A0D-F431-4C29-9FDD-E39C38A3D027}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85EF9A0D-F431-4C29-9FDD-E39C38A3D027}.Release|Any CPU.Build.0 = Release|Any CPU + {04987A3C-032F-49CB-B59A-80D188BF1372}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04987A3C-032F-49CB-B59A-80D188BF1372}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04987A3C-032F-49CB-B59A-80D188BF1372}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04987A3C-032F-49CB-B59A-80D188BF1372}.Release|Any CPU.Build.0 = Release|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Release|Any CPU.Build.0 = Release|Any CPU + {395822D5-7405-41B8-AC00-9106BF00EB76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {395822D5-7405-41B8-AC00-9106BF00EB76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {395822D5-7405-41B8-AC00-9106BF00EB76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {395822D5-7405-41B8-AC00-9106BF00EB76}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace/AuthContext.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace/AuthContext.cs new file mode 100644 index 00000000..844135ae --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace/AuthContext.cs @@ -0,0 +1,8 @@ +namespace Lab.Context.Trace; + +public record AuthContext +{ + public string TraceId { get; init; } + + public string UserId { get; init; } +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace/ContextAccessor.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace/ContextAccessor.cs new file mode 100644 index 00000000..60465637 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace/ContextAccessor.cs @@ -0,0 +1,17 @@ +namespace Lab.Context.Trace; + +public class ContextAccessor : IContextSetter, IContextGetter + where T : class +{ + private T _value; + + public void Set(T value) + { + this._value = value; + } + + public T? Get() + { + return this._value; + } +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace/ContextAccessor2.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace/ContextAccessor2.cs new file mode 100644 index 00000000..a706f605 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace/ContextAccessor2.cs @@ -0,0 +1,23 @@ +namespace Lab.Context.Trace; + +public class ContextAccessor2 : IContextSetter, IContextGetter + where T : class +{ + private static readonly AsyncLocal> s_current = new(); + + public T? Get() + { + var contextHolder = s_current.Value; + return contextHolder?.Value; + } + + public void Set(T value) + { + if (s_current.Value == null) + { + s_current.Value = new ContextHolder(); + } + + s_current.Value.Value = value; + } +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace/ContextHolder.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace/ContextHolder.cs new file mode 100644 index 00000000..3a0238d8 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace/ContextHolder.cs @@ -0,0 +1,6 @@ +namespace Lab.Context.Trace; + +public class ContextHolder +{ + public T Value { get; set; } +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace/IContextGetter.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace/IContextGetter.cs new file mode 100644 index 00000000..315ffb41 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace/IContextGetter.cs @@ -0,0 +1,6 @@ +namespace Lab.Context.Trace; + +public interface IContextGetter +{ + T? Get(); +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace/IContextSetter.cs b/Trace/Lab.Context.Trace/Lab.Context.Trace/IContextSetter.cs new file mode 100644 index 00000000..a9b95902 --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace/IContextSetter.cs @@ -0,0 +1,6 @@ +namespace Lab.Context.Trace; + +public interface IContextSetter +{ + void Set(T value); +} \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/Lab.Context.Trace/Lab.Context.Trace.csproj b/Trace/Lab.Context.Trace/Lab.Context.Trace/Lab.Context.Trace.csproj new file mode 100644 index 00000000..0c1dcdeb --- /dev/null +++ b/Trace/Lab.Context.Trace/Lab.Context.Trace/Lab.Context.Trace.csproj @@ -0,0 +1,16 @@ + + + + net7.0 + enable + enable + Linux + + + + + + + + + diff --git a/Trace/Lab.Context.Trace/docker-compose.yml b/Trace/Lab.Context.Trace/docker-compose.yml new file mode 100644 index 00000000..a9338570 --- /dev/null +++ b/Trace/Lab.Context.Trace/docker-compose.yml @@ -0,0 +1,7 @@ +services: + seq: + image: datalust/seq:latest + ports: + - "5341:80" + environment: + - ACCEPT_EULA=Y \ No newline at end of file diff --git a/Trace/Lab.Context.Trace/k8s/ns.yml b/Trace/Lab.Context.Trace/k8s/ns.yml new file mode 100644 index 00000000..78c095b8 --- /dev/null +++ b/Trace/Lab.Context.Trace/k8s/ns.yml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: Title +spec: + selector: + app: Title + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: NodePort \ No newline at end of file diff --git a/WebAPI/Idempotent/.editorconfig b/WebAPI/Idempotent/.editorconfig new file mode 100644 index 00000000..9cb23b81 --- /dev/null +++ b/WebAPI/Idempotent/.editorconfig @@ -0,0 +1,464 @@ +root = true +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file + +# Don't use tabs for indentation. +[*] +indent_style = space +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false +indent_size = 4 + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined +dotnet_naming_rule.constants_rule.severity = warning +dotnet_naming_rule.constants_rule.style = upper_camel_case_style +dotnet_naming_rule.constants_rule.symbols = constants_symbols +dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = s_lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = s_lower_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_rule.public_fields_rule.import_to_resharper = as_predefined +dotnet_naming_rule.public_fields_rule.resharper_style = AaBb, _ + aaBb +dotnet_naming_rule.public_fields_rule.severity = warning +dotnet_naming_rule.public_fields_rule.style = upper_camel_case_style +dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols +dotnet_naming_rule.static_readonly_rule.import_to_resharper = as_predefined +dotnet_naming_rule.static_readonly_rule.severity = warning +dotnet_naming_rule.static_readonly_rule.style = all_upper_style +dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols +dotnet_naming_rule.type_parameters_rule.import_to_resharper = as_predefined +dotnet_naming_rule.type_parameters_rule.severity = warning +dotnet_naming_rule.type_parameters_rule.style = upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols +dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule.severity = warning +dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols +dotnet_naming_style.all_upper_style.capitalization = all_upper +dotnet_naming_style.all_upper_style.word_separator = _ +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.s_lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.s_lower_camel_case_style.required_prefix = s_ +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.constants_symbols.applicable_kinds = field +dotnet_naming_symbols.constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static, readonly +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = true:suggestion +dotnet_style_qualification_for_field = true:suggestion +dotnet_style_qualification_for_method = true:suggestion +dotnet_style_qualification_for_property = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# ReSharper properties +resharper_align_linq_query = true +resharper_align_multiline_extends_list = true +resharper_align_multiline_for_stmt = true +resharper_align_multiline_statement_conditions = false +resharper_align_multiple_declaration = true +resharper_align_multline_type_parameter_constrains = true +resharper_align_multline_type_parameter_list = true +resharper_apply_on_completion = true +resharper_autodetect_indent_settings = true +resharper_blank_lines_after_control_transfer_statements = 1 +resharper_blank_lines_around_single_line_auto_property = 1 +resharper_blank_lines_around_single_line_property = 1 +resharper_blank_lines_before_single_line_comment = 1 +resharper_braces_for_for = required +resharper_braces_for_foreach = required +resharper_braces_for_ifelse = required +resharper_braces_for_while = required +resharper_csharp_alignment_tab_fill_style = optimal_fill +resharper_csharp_blank_lines_around_single_line_invocable = 1 +resharper_csharp_int_align_comments = true +resharper_csharp_keep_blank_lines_in_code = 1 +resharper_csharp_keep_blank_lines_in_declarations = 1 +resharper_csharp_outdent_commas = true +resharper_csharp_stick_comment = false +resharper_enforce_line_ending_style = true +resharper_indent_braces_inside_statement_conditions = false +resharper_indent_pars = outside +resharper_int_align_methods = true +resharper_int_align_nested_ternary = true +resharper_parentheses_redundancy_style = remove +resharper_place_accessorholder_attribute_on_same_line = false +resharper_show_autodetect_configure_formatting_tip = false +resharper_use_continuous_indent_inside_initializer_braces = false +resharper_use_continuous_indent_inside_parens = false +resharper_use_indent_from_vs = false +resharper_wrap_array_initializer_style = chop_if_long + +# ReSharper inspection severities +resharper_arrange_redundant_parentheses_highlighting = suggestion +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8 + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Powershell files +[*.ps1] +indent_size = 2 + +# Shell script files +[*.sh] +indent_size = 2 + +# Template file +[*.gpl] +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] + +# IDE0055: Fix formatting +dotnet_diagnostic.ide0055.severity = warning + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = true +dotnet_style_qualification_for_property = true +dotnet_style_qualification_for_method = true +dotnet_style_qualification_for_event = true + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Whitespace options +dotnet_style_allow_multiple_blank_lines_experimental = false + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Non-private readonly fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case + +# Constants are PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = pascal_case + +# Static fields are camelCase and start with s_ +dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_field_style.capitalization = camel_case +dotnet_naming_style.static_field_style.required_prefix = s_ + +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +# By default, name items with PascalCase +dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.all_members.applicable_kinds = * + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}' +dotnet_diagnostic.rs2008.severity = none + +# IDE0073: File header +# dotnet_diagnostic.IDE0073.severity = warning +# file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license.\nSee the LICENSE file in the project root for more information. + +# IDE0035: Remove unreachable code +dotnet_diagnostic.ide0035.severity = warning + +# IDE0036: Order modifiers +dotnet_diagnostic.ide0036.severity = warning + +# IDE0043: Format string contains invalid placeholder +dotnet_diagnostic.ide0043.severity = warning + +# IDE0044: Make field readonly +dotnet_diagnostic.ide0044.severity = warning + +# RS0016: Only enable if API files are present +dotnet_public_api_analyzer.require_api_files = true + +# CSharp code style settings: +[*.cs] +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +# csharp_new_line_before_members_in_object_initializers = true +# csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Whitespace options +csharp_style_allow_embedded_statements_on_same_line_experimental = false +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Blocks are allowed +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +# Currently only enabled for C# due to crash in VB analyzer. VB can be enabled once +# https://github.com/dotnet/roslyn/pull/54259 has been published. +dotnet_style_allow_statement_immediately_after_block_experimental = false + +[src/CodeStyle/**.{cs,vb}] +# warning RS0005: Do not use generic CodeAction.Create to create CodeAction +dotnet_diagnostic.rs0005.severity = none + +[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures,VisualStudio}/**/*.{cs,vb}] + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline:warning +# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201 +dotnet_diagnostic.ide0011.severity = warning + +# IDE0040: Add accessibility modifiers +dotnet_diagnostic.ide0040.severity = warning + +# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings? +# IDE0051: Remove unused private member +dotnet_diagnostic.ide0051.severity = warning + +# IDE0052: Remove unread private member +dotnet_diagnostic.ide0052.severity = warning + +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.ide0059.severity = warning + +# IDE0060: Remove unused parameter +dotnet_diagnostic.ide0060.severity = warning + +# CA1012: Abstract types should not have public constructors +dotnet_diagnostic.ca1012.severity = warning + +# CA1822: Make member static +dotnet_diagnostic.ca1822.severity = warning + +# Prefer "var" everywhere +dotnet_diagnostic.ide0007.severity = warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true:warning + +# dotnet_style_allow_multiple_blank_lines_experimental +dotnet_diagnostic.ide2000.severity = warning + +# csharp_style_allow_embedded_statements_on_same_line_experimental +dotnet_diagnostic.ide2001.severity = warning + +# csharp_style_allow_blank_lines_between_consecutive_braces_experimental +dotnet_diagnostic.ide2002.severity = warning + +# dotnet_style_allow_statement_immediately_after_block_experimental +dotnet_diagnostic.ide2003.severity = warning + +# csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental +dotnet_diagnostic.ide2004.severity = warning + +[src/{VisualStudio}/**/*.{cs,vb}] +# CA1822: Make member static +# There is a risk of accidentally breaking an internal API that partners rely on though IVT. +dotnet_code_quality.ca1822.api_surface = private + +[{*.har,*.inputactions,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}] +indent_style = space +indent_size = 2 + +[{*.yaml,*.yml}] +indent_style = space +indent_size = 2 + +[*.csv] +indent_style = tab +tab_width = 1 + +[*.{appxmanifest,asax,ascx,aspx,axaml,build,c,c++,cc,cginc,compute,cp,cpp,cs,cshtml,cu,cuh,cxx,dtd,feature,fs,fsi,fsscript,fsx,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,master,ml,mli,mpp,mq4,mq5,mqh,nuspec,paml,razor,resw,resx,shader,skin,tpp,usf,ush,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 diff --git a/WebAPI/Idempotent/.gitignore b/WebAPI/Idempotent/.gitignore new file mode 100644 index 00000000..81c554f7 --- /dev/null +++ b/WebAPI/Idempotent/.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/WebAPI/Idempotent/Lab.Idempotent.sln b/WebAPI/Idempotent/Lab.Idempotent.sln new file mode 100644 index 00000000..75b40d4c --- /dev/null +++ b/WebAPI/Idempotent/Lab.Idempotent.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{97B4E8A6-F946-4D97-87E8-6ED45319A07F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{FABC7D61-6407-48E3-BC03-E1618276A877}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.Idempotent.WebApi", "src\Lab.Idempotent.WebApi\Lab.Idempotent.WebApi.csproj", "{EA97918D-52E7-4DD2-A2F5-5B5A76FED4FF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{87BA93CC-B40F-45DC-BB6D-3393B443C7CD}" + 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(NestedProjects) = preSolution + {EA97918D-52E7-4DD2-A2F5-5B5A76FED4FF} = {97B4E8A6-F946-4D97-87E8-6ED45319A07F} + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EA97918D-52E7-4DD2-A2F5-5B5A76FED4FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA97918D-52E7-4DD2-A2F5-5B5A76FED4FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA97918D-52E7-4DD2-A2F5-5B5A76FED4FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA97918D-52E7-4DD2-A2F5-5B5A76FED4FF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebAPI/Idempotent/Taskfile.yml b/WebAPI/Idempotent/Taskfile.yml new file mode 100644 index 00000000..532f9520 --- /dev/null +++ b/WebAPI/Idempotent/Taskfile.yml @@ -0,0 +1,15 @@ +# Taskfile.yml + +version: "3" + +dotenv: [ "secrets/secrets.env" ] + +tasks: + ## Develop --------------------------------------------------- + dev-init: + desc: Init development environment + cmds: + - task: redis-start + + + \ No newline at end of file diff --git a/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/.dockerignore b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/.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/Idempotent/src/Lab.Idempotent.WebApi/Controllers/WeatherForecastController.cs b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..a8a97a14 --- /dev/null +++ b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Controllers/WeatherForecastController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.Idempotent.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 static readonly List s_repository = new(); + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpPost("{temperature}")] + [Idempotent] + public async Task> Post(int temperature, CancellationToken cancel = default) + { + var rng = new Random(); + var data = new WeatherForecast + { + TemperatureC = temperature, + Summary = Summaries[rng.Next(Summaries.Length)], + Date = DateTime.UtcNow + }; + s_repository.Add(data); + + return data; + } + + [HttpGet] + public async Task>> Get() + { + return s_repository; + } +} diff --git a/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Dockerfile b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Dockerfile new file mode 100644 index 00000000..f6db24e1 --- /dev/null +++ b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/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.Idempotent.WebApi/Lab.Idempotent.WebApi.csproj", "Lab.Idempotent.WebApi/"] +RUN dotnet restore "src/Lab.Idempotent.WebApi/Lab.Idempotent.WebApi.csproj" +COPY . . +WORKDIR "/src/Lab.Idempotent.WebApi" +RUN dotnet build "Lab.Idempotent.WebApi.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Lab.Idempotent.WebApi.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Lab.Idempotent.WebApi.dll"] diff --git a/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Failure.cs b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Failure.cs new file mode 100644 index 00000000..5b73e80a --- /dev/null +++ b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Failure.cs @@ -0,0 +1,39 @@ +using System.Net; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.Idempotent.WebApi; + +public enum FailureCode +{ + NotFoundIdempotentKey, +} + +public class Failure +{ + public static IReadOnlyDictionary Results = new Dictionary() + { + { + FailureCode.NotFoundIdempotentKey,new ObjectResult(new Failure + { + Code = FailureCode.NotFoundIdempotentKey.ToString(), + Message = "Not found Idempotent key in header", + Data = new + { + Property = "IdempotentKey", + Value = "" + }, + }) + { + StatusCode = (int)HttpStatusCode.BadRequest + } + }, + }; + + public string Code { get; set; } + + public string Message { get; set; } + + public object Data { get; set; } + + public IEnumerable Failures { get; set; } +} diff --git a/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/IdempotentAttributeFilter.cs b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/IdempotentAttributeFilter.cs new file mode 100644 index 00000000..842451f3 --- /dev/null +++ b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/IdempotentAttributeFilter.cs @@ -0,0 +1,98 @@ +using System.Net; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Caching.Distributed; + +namespace Lab.Idempotent.WebApi; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)] +public class IdempotentAttribute : Attribute, IFilterFactory +{ + public bool IsReusable => false; + + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + var distributedCache = (IDistributedCache)serviceProvider.GetService(typeof(IDistributedCache)); + + var filter = new IdempotentAttributeFilter(distributedCache); + return filter; + } +} + +public class IdempotentAttributeFilter : ActionFilterAttribute +{ + public const string HeaderName = "IdempotencyKey"; + public static readonly TimeSpan Expiration = new(0, 0, 60); + + private readonly IDistributedCache _distributedCache; + private string _idempotencyKey; + private bool _hasIdempotencyKey; + + public IdempotentAttributeFilter(IDistributedCache distributedCache) + { + this._distributedCache = distributedCache; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + // 檢查 Header 有沒有 IdempotencyKey + if (context.HttpContext.Request.Headers.TryGetValue(HeaderName, out var idempotencyKey) == false) + { + // 沒有的話則回傳 Bad Request + context.Result = Failure.Results[FailureCode.NotFoundIdempotentKey]; + return; + } + + this._idempotencyKey = idempotencyKey; + + var cacheData = this._distributedCache.GetString(this.GetDistributedCacheKey()); + if (cacheData == null) + { + // 沒有快取則進入 Action + return; + } + + // 從快取取出內容回傳給調用端 + var jsonObject = JsonObject.Parse(cacheData); + context.Result = new ObjectResult(jsonObject["Data"]) + { + StatusCode = jsonObject["StatusCode"].GetValue() + }; + this._hasIdempotencyKey = true; + } + + public override void OnResultExecuted(ResultExecutedContext context) + { + if (this._hasIdempotencyKey) + { + return; + } + + // 把回傳結果放到快取裡面 + var contextResult = (ObjectResult)context.Result; + if (contextResult.StatusCode != (int)HttpStatusCode.OK) + { + return; + } + + var cacheOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = Expiration + }; + var json = JsonSerializer.Serialize(new + { + Data = contextResult.Value, + contextResult.StatusCode + }); + this._distributedCache.SetString(this.GetDistributedCacheKey(), + json, + cacheOptions); + } + + private string GetDistributedCacheKey() + { + return "IdempotencyKey:" + this._idempotencyKey; + } +} diff --git a/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Lab.Idempotent.WebApi.csproj b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Lab.Idempotent.WebApi.csproj new file mode 100644 index 00000000..0db9dec4 --- /dev/null +++ b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Lab.Idempotent.WebApi.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + Linux + + + + + + + diff --git a/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Program.cs b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Program.cs new file mode 100644 index 00000000..9bd7d200 --- /dev/null +++ b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/Program.cs @@ -0,0 +1,44 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Unicode; + +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.AddDistributedMemoryCache(p => +{ + p.ExpirationScanFrequency = TimeSpan.FromSeconds(60); +}); +builder.Services.AddSingleton(p => new JsonSerializerOptions +{ + Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs), + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter() } +}); + +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(); diff --git a/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/WeatherForecast.cs b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/WeatherForecast.cs new file mode 100644 index 00000000..e032b21b --- /dev/null +++ b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Lab.Idempotent.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; } +} diff --git a/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/appsettings.Development.json b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/appsettings.json b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/WebAPI/Idempotent/src/Lab.Idempotent.WebApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} 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.BasicAuthentication/Authentication/BasicAuthenticationDefaults.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationDefaults.cs new file mode 100644 index 00000000..b828bf60 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationDefaults.cs @@ -0,0 +1,6 @@ +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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.BasicAuthentication/Authentication/BasicAuthenticationExtensions.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationExtensions.cs new file mode 100644 index 00000000..d075a779 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationExtensions.cs @@ -0,0 +1,33 @@ +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Lab.AspNetCore.Security.BasicAuthentication; + +public static class BasicAuthenticationExtensions +{ + public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder builder, + string authenticationScheme, + string displayName, + Action configureOptions) + where TAuthProvider : class, IBasicAuthenticationProvider + { + builder.Services + .AddSingleton, BasicAuthenticationPostConfigureOptions>(); + builder.Services.AddSingleton(); + + return builder.AddScheme( + authenticationScheme, + displayName, + configureOptions); + } + public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder builder, + string authenticationScheme, + Action configureOptions) + where TAuthProvider : class, IBasicAuthenticationProvider + { + return AddBasicAuthentication(builder, authenticationScheme, null, configureOptions); + } + +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationHandler.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationHandler.cs new file mode 100644 index 00000000..1337b303 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationHandler.cs @@ -0,0 +1,114 @@ +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; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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; + } + + 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[HeaderNames.WWWAuthenticate] = $"Basic realm=\"{this.Options.Realm}\", charset=\"UTF-8\""; + + // 響應粗糙的內容,這不是標準的 Basic Authentication 失敗的回傳,僅是為了示意 + // this.Response.WriteAsJsonAsync(new + // { + // Code = "InvalidAuthentication", + // Message = "Please contact your administrator" + // }); + await base.HandleChallengeAsync(properties); + } + + 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.BasicAuthentication/Authentication/BasicAuthenticationOptions.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationOptions.cs new file mode 100644 index 00000000..1056f1eb --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationOptions.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authentication; + +namespace Lab.AspNetCore.Security.BasicAuthentication; + +public class BasicAuthenticationOptions : AuthenticationSchemeOptions +{ + public string Realm { get; set; } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationPostConfigureOptions.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationPostConfigureOptions.cs new file mode 100644 index 00000000..689c95df --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/BasicAuthenticationPostConfigureOptions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Options; + +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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.BasicAuthentication/Authentication/DefaultBasicAuthenticationProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/DefaultBasicAuthenticationProvider.cs new file mode 100644 index 00000000..4d27578d --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/DefaultBasicAuthenticationProvider.cs @@ -0,0 +1,9 @@ +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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.BasicAuthentication/Authentication/IBasicAuthenticationProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/IBasicAuthenticationProvider.cs new file mode 100644 index 00000000..1e5b3c13 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authentication/IBasicAuthenticationProvider.cs @@ -0,0 +1,6 @@ +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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.BasicAuthentication/Authorization/IPermissionAuthorizationProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/IPermissionAuthorizationProvider.cs new file mode 100644 index 00000000..35ede6e1 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/IPermissionAuthorizationProvider.cs @@ -0,0 +1,6 @@ +namespace Lab.AspNetCore.Security.BasicAuthentication; + +public interface IPermissionAuthorizationProvider +{ + IEnumerable GetPermissions(string userId); +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/Permission.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/Permission.cs new file mode 100644 index 00000000..fbdf88b6 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/Permission.cs @@ -0,0 +1,22 @@ +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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.BasicAuthentication/Authorization/PermissionAuthorizationHandler.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationHandler.cs new file mode 100644 index 00000000..5647342f --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationHandler.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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.BasicAuthentication/Authorization/PermissionAuthorizationMiddlewareResultHandler.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationMiddlewareResultHandler.cs new file mode 100644 index 00000000..3e0d3d12 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationMiddlewareResultHandler.cs @@ -0,0 +1,56 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Lab.AspNetCore.Security.BasicAuthentication; + +public class PermissionAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler +{ + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly AuthorizationMiddlewareResultHandler _defaultHandler = new(); + + 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 this._defaultHandler.HandleAsync(next, context, policy, authorizeResult); + + // await next.Invoke(context); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationPolicyProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationPolicyProvider.cs new file mode 100644 index 00000000..d169dd2b --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationPolicyProvider.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; + +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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. + this.FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options); + } + + public Task GetDefaultPolicyAsync() => this.FallbackPolicyProvider.GetDefaultPolicyAsync(); + + public Task GetFallbackPolicyAsync() => this.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 this.FallbackPolicyProvider.GetPolicyAsync(policyName); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationProvider.cs new file mode 100644 index 00000000..9dceacc7 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationProvider.cs @@ -0,0 +1,21 @@ +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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.BasicAuthentication/Authorization/PermissionAuthorizationRequirement.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationRequirement.cs new file mode 100644 index 00000000..910da0ed --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Authorization/PermissionAuthorizationRequirement.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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.BasicAuthentication/FieldTypeAssistant.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/FieldTypeAssistant.cs new file mode 100644 index 00000000..5b489664 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/FieldTypeAssistant.cs @@ -0,0 +1,41 @@ +using System.Collections.Concurrent; +using System.Reflection; + +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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.BasicAuthentication/Lab.AspNetCore.Security.BasicAuthentication.csproj b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Lab.AspNetCore.Security.BasicAuthentication.csproj new file mode 100644 index 00000000..e4190c84 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthentication/Lab.AspNetCore.Security.BasicAuthentication.csproj @@ -0,0 +1,17 @@ + + + net6.0 + enable + enable + + + + + + + + + + + + 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..a17358bc --- /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,167 @@ +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.Options; +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(); + var authenticationHandler = 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.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.AddBasicAuthentication(o => { o.Realm = "Test"; }); + 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.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..e3da0a29 --- /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,74 @@ +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.AddBasicAuthentication(o => { o.Realm = "Test"; }); + 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..2bc36483 --- /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) + { + _logger = logger; + } + + [HttpGet] + [Authorize] + public ActionResult Get() + { + return this.Ok("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..58b7e218 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Lab.AspNetCore.Security.BasicAuthenticationSite.csproj @@ -0,0 +1,14 @@ + + + + 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..c3b89d50 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Program.cs @@ -0,0 +1,56 @@ +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; + +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.AddBasicAuthentication(o => o.Realm = "Basic Authentication"); +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.AddSingleton(); +// builder.Services.AddSingleton(); +// builder.Services.AddSingleton(); +// +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseStaticFiles(); +app.UseStatusCodePages(); +app.UseHttpsRedirection(); +app.UseRouting(); + +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..3018ce92 --- /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": false, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7089;http://localhost:5089", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "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..5367317d --- /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..ca76b952 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; + +public static class BasicAuthenticationExtensions +{ + public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, + string authenticationScheme, + string displayName, + Action configureOptions) + where TAuthProvider : class, IBasicAuthenticationProvider + { + builder.Services + .AddSingleton, BasicAuthenticationPostConfigureOptions>(); + builder.Services.AddSingleton(); + + return builder.AddScheme( + authenticationScheme, + displayName, + configureOptions); + } + + public static AuthenticationBuilder AddBasicAuthentication(this IServiceCollection services, + Action configureOptions) + where TAuthProvider : class, IBasicAuthenticationProvider + { + var scheme = BasicAuthenticationDefaults.AuthenticationScheme; + return services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = scheme; + o.DefaultChallengeScheme = scheme; + }) + .AddBasic(scheme, scheme, 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..2da5bc36 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authentication/BasicAuthenticationHandler.cs @@ -0,0 +1,112 @@ +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; + } + + 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[HeaderNames.WWWAuthenticate] = $"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..f2371e81 --- /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; } +} \ 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..690eaba0 --- /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..ff5d36fd --- /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..6ba99ad0 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.BasicAuthenticationSite/Security/Authorization/PermissionAuthorizationMiddlewareResultHandler.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +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; + private readonly AuthorizationMiddlewareResultHandler _defaultHandler = new(); + + 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 this._defaultHandler.HandleAsync(next, context, policy, authorizeResult); + + // await next.Invoke(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.MultiAuthenticationSite/ApiKeyProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/ApiKeyProvider.cs new file mode 100644 index 00000000..ba321ad2 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/ApiKeyProvider.cs @@ -0,0 +1,37 @@ +using System.Security.Claims; +using AspNetCore.Authentication.ApiKey; + +namespace Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; + +public class ApiKey : IApiKey +{ + public string Key { get; init; } + + public string OwnerName { get; init; } + + public IReadOnlyCollection Claims { get; init; } +} + +public class ApiKeyProvider : IApiKeyProvider +{ + private readonly ILogger _logger; + + public ApiKeyProvider(ILogger logger) + { + _logger = logger; + } + + public async Task ProvideAsync(string key) + { + var result = new ApiKey + { + Key = "9527", + OwnerName = "yao", + Claims = new List() + { + new(ClaimTypes.Name, "yao") + } + }; + return result; + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/BasicAuthenticationProvider.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/BasicAuthenticationProvider.cs new file mode 100644 index 00000000..8940a26b --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/BasicAuthenticationProvider.cs @@ -0,0 +1,24 @@ +namespace Lab.AspNetCore.Security.BasicAuthentication; + +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.MultiAuthenticationSite/Controllers/DemoController.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/Controllers/DemoController.cs new file mode 100644 index 00000000..ef88c969 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/Controllers/DemoController.cs @@ -0,0 +1,27 @@ +using AspNetCore.Authentication.ApiKey; +using Lab.AspNetCore.Security.BasicAuthentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.AspNetCore.Security.MultiAuthenticationSite.Controllers; + +[ApiController] +[Route("[controller]")] +public class DemoController : ControllerBase +{ + private readonly ILogger _logger; + + public DemoController(ILogger logger) + { + _logger = logger; + } + + + // [Authorize(AuthenticationSchemes = BasicAuthenticationDefaults.AuthenticationScheme)] + // [Authorize(AuthenticationSchemes = ApiKeyDefaults.AuthenticationScheme)] + [Authorize] + public ActionResult Get() + { + return this.Ok("OK~好"); + } +} \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/Lab.AspNetCore.Security.MultiAuthenticationSite.csproj b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/Lab.AspNetCore.Security.MultiAuthenticationSite.csproj new file mode 100644 index 00000000..7afd3fbe --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/Lab.AspNetCore.Security.MultiAuthenticationSite.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/Program.cs b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/Program.cs new file mode 100644 index 00000000..fd836b47 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/Program.cs @@ -0,0 +1,67 @@ +using AspNetCore.Authentication.ApiKey; +using Lab.AspNetCore.Security.BasicAuthentication; +using Lab.AspNetCore.Security.BasicAuthenticationSite.Security.Authentication; +using Microsoft.Net.Http.Headers; + +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 multiScheme = "MultiAuthSchemes"; +builder.Services.AddAuthentication(p => + { + p.DefaultScheme = multiScheme; + p.DefaultChallengeScheme = multiScheme; + }) + .AddApiKeyInHeaderOrQueryParams(p => + { + p.Realm = "Sample Web API"; + p.KeyName = "X-API-KEY"; + }) + .AddBasicAuthentication(BasicAuthenticationDefaults.AuthenticationScheme, + p => + { + p.Realm = "Basic Authentication"; + }) + .AddPolicyScheme(multiScheme, ApiKeyDefaults.AuthenticationScheme, p => + { + p.ForwardDefaultSelector = context => + { + string authorization = context.Request.Headers[HeaderNames.Authorization]; + if (string.IsNullOrEmpty(authorization) == false && + authorization.StartsWith($"{BasicAuthenticationDefaults.AuthenticationScheme} ", + StringComparison.InvariantCultureIgnoreCase)) + { + return BasicAuthenticationDefaults.AuthenticationScheme; + } + + return ApiKeyDefaults.AuthenticationScheme; + }; + }) + ; + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseStaticFiles(); +app.UseStatusCodePages(); +app.UseHttpsRedirection(); +app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/appsettings.Development.json b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/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.MultiAuthenticationSite/appsettings.json b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.MultiAuthenticationSite/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..d4e84f3d --- /dev/null +++ b/WebAPI/Security/Lab.AspNetCore.Security/Lab.AspNetCore.Security.sln @@ -0,0 +1,40 @@ + +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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.AspNetCore.Security.MultiAuthenticationSite", "Lab.AspNetCore.Security.MultiAuthenticationSite\Lab.AspNetCore.Security.MultiAuthenticationSite.csproj", "{FB32C9D2-A7C7-4199-9477-AA5EEA7D818F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.AspNetCore.Security.BasicAuthentication", "Lab.AspNetCore.Security.BasicAuthentication\Lab.AspNetCore.Security.BasicAuthentication.csproj", "{CE3A118F-BBE8-475A-86BF-A37ED4C1E1F8}" +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 + {FB32C9D2-A7C7-4199-9477-AA5EEA7D818F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB32C9D2-A7C7-4199-9477-AA5EEA7D818F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB32C9D2-A7C7-4199-9477-AA5EEA7D818F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB32C9D2-A7C7-4199-9477-AA5EEA7D818F}.Release|Any CPU.Build.0 = Release|Any CPU + {CE3A118F-BBE8-475A-86BF-A37ED4C1E1F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE3A118F-BBE8-475A-86BF-A37ED4C1E1F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE3A118F-BBE8-475A-86BF-A37ED4C1E1F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE3A118F-BBE8-475A-86BF-A37ED4C1E1F8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/Lab.RefitClient.TestProject.csproj b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/Lab.RefitClient.TestProject.csproj new file mode 100644 index 00000000..bb13570c --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/Lab.RefitClient.TestProject.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/PetStoreTestServer.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/PetStoreTestServer.cs new file mode 100644 index 00000000..4fa7f315 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/PetStoreTestServer.cs @@ -0,0 +1,20 @@ +using Lab.RefitClient.WebAPI; +using Lab.RefitClient.WebAPI.Controllers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Lab.RefitClient.TestProject; + +public class PetStoreTestServer : WebApplicationFactory +{ + private void ConfigureServices(IServiceCollection services) + { + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(this.ConfigureServices); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/UnitTest1.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/UnitTest1.cs new file mode 100644 index 00000000..e73fa23b --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/UnitTest1.cs @@ -0,0 +1,48 @@ +using Lab.RefitClient.GeneratedCode.PetStore; +using Microsoft.Extensions.DependencyInjection; +using Refit; + +namespace Lab.RefitClient.TestProject; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public async Task RestServiceFor() + { + var server = new PetStoreTestServer(); + var httpClient = server.CreateClient(); + httpClient.DefaultRequestHeaders.Add(PetStoreHeaderNames.IdempotencyKey, "1234567890"); + httpClient.DefaultRequestHeaders.Add(PetStoreHeaderNames.ApiKey, "1234567890"); + httpClient.BaseAddress = new Uri(httpClient.BaseAddress, "api/v3"); + var client = RestService.For(httpClient); + var username = "yao"; + + var response = await client.GetUserByName(username); + var content = response.Content; + Assert.AreEqual(username, content.Username); + } + + /// + /// F5執行WebApi專案_再呼叫WebApi + /// + [TestMethod] + public async Task AddRefitClient() + { + var baseUrl = "https://localhost:7285/api/v3"; + var services = new ServiceCollection(); + services.AddRefitClient() + .ConfigureHttpClient(p => + { + p.DefaultRequestHeaders.Add(PetStoreHeaderNames.IdempotencyKey, "1234567890"); + p.DefaultRequestHeaders.Add(PetStoreHeaderNames.ApiKey, "1234567890"); + p.BaseAddress = new Uri(baseUrl); + }); + var serviceProvider = services.BuildServiceProvider(); + var client = serviceProvider.GetService(); + var username = "yao"; + var response = await client.GetUserByName(username); + var content = response.Content; + Assert.AreEqual(username, content.Username); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/UnitTest2.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/UnitTest2.cs new file mode 100644 index 00000000..0ba4e539 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/UnitTest2.cs @@ -0,0 +1,95 @@ +using System.Net; +using Lab.RefitClient.GeneratedCode.PetStore; +using Microsoft.Extensions.DependencyInjection; +using Refit; + +namespace Lab.RefitClient.TestProject; + +[TestClass] +public class UnitTest2 +{ + [TestMethod] + public async Task RestServiceFor() + { + var contextAccessor = new ContextAccessor(); + var server = new PetStoreTestServer(); + var httpClient = server.CreateDefaultClient(new DefaultHeaderHandler(contextAccessor) + { + InnerHandler = new SocketsHttpHandler() + }); + httpClient.BaseAddress = new Uri(httpClient.BaseAddress, "api/v3"); + + var client = RestService.For(httpClient); + + var username = "yao"; + this.SetHeaderContext(contextAccessor); + var response = await client.GetUserByName(username); + var content = response.Content; + Console.WriteLine("get first headers: {0}", response.Headers); + Assert.AreEqual(username, content.Username); + Thread.Sleep(1000); + + this.SetHeaderContext(contextAccessor); + var response1 = await client.GetUserByName(username); + var content1 = response1.Content; + Console.WriteLine("get second headers: {0}", response1.Headers); + Assert.AreEqual(username, content1.Username); + } + + void SetHeaderContext(IContextSetter setter) + { + var key = DateTime.Now.ToString("yyyy/MM/dd hh:mm:ss.fff"); + var headerContext = new HeaderContext + { + IdempotencyKey = key, + ApiKey = key + }; + setter.Set(headerContext); + } + + [TestMethod] + public async Task AddRefitClient() + { + var baseUrl = "https://localhost:7285/api/v3"; + + var services = new ServiceCollection(); + + services.AddSingleton>(); + services.AddSingleton>(p => p.GetService>()); + services.AddSingleton>(p => p.GetService>()); + services.AddSingleton(p => + { + var settings = new RefitSettings + { + HttpMessageHandlerFactory = () => + new DefaultHeaderHandler(p.GetService>()) + { + InnerHandler = new SocketsHttpHandler() + }, + }; + return settings; + }); + + services.AddRefitClient(p => p.GetRequiredService()) + .ConfigureHttpClient(p => { p.BaseAddress = new Uri(baseUrl); }) + ; + + var serviceProvider = services.BuildServiceProvider(); + var contextSetter = serviceProvider.GetService>(); + var client = serviceProvider.GetService(); + var username = "yao"; + + this.SetHeaderContext(contextSetter); + var response = await client.GetUserByName(username); + var content = response.Content; + Console.WriteLine("get first headers: {0}", response.Headers); + Assert.AreEqual(username, content.Username); + Thread.Sleep(1000); + + this.SetHeaderContext(contextSetter); + var response1 = await client.GetUserByName(username); + var content1 = response1.Content; + Console.WriteLine("get second headers: {0}", response1.Headers); + Assert.AreEqual(username, content1.Username); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/Usings.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.TestProject/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/Controllers/DemoController.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/Controllers/DemoController.cs new file mode 100644 index 00000000..8b016a0f --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/Controllers/DemoController.cs @@ -0,0 +1,31 @@ +using Lab.RefitClient.GeneratedCode.PetStore; +using Microsoft.AspNetCore.Mvc; + +namespace Lab.RefitClient.WebAPI2.Controllers; + +[ApiController] +[Route("[controller]")] +public class DemoController : ControllerBase +{ + private readonly ISwaggerPetstoreOpenAPI30 _petStoreService; + private readonly ILogger _logger; + + public DemoController(ILogger logger, + ISwaggerPetstoreOpenAPI30 petStoreService) + { + this._logger = logger; + this._petStoreService = petStoreService; + } + + [HttpGet("{name}", Name = "GetUserName")] + public async Task Get(string name) + { + var response = await this._petStoreService.GetUserByName(name); + var user = response.Content; + var idempotencyKey = response.Headers.GetValues(PetStoreHeaderNames.IdempotencyKey).FirstOrDefault(); + var apiKey = response.Headers.GetValues(PetStoreHeaderNames.ApiKey).FirstOrDefault(); + this.Response.Headers[PetStoreHeaderNames.IdempotencyKey]= idempotencyKey; + this.Response.Headers[PetStoreHeaderNames.ApiKey]= apiKey; + return this.Ok(user); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/GenHeaderContextFilterAttribute.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/GenHeaderContextFilterAttribute.cs new file mode 100644 index 00000000..20a01720 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/GenHeaderContextFilterAttribute.cs @@ -0,0 +1,20 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Lab.RefitClient.WebAPI +{ + public class GenHeaderContextFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext actionContext) + { + var key = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"); + + var headerContext = actionContext.HttpContext.RequestServices.GetService>(); + headerContext.Set(new HeaderContext + { + IdempotencyKey = key, + ApiKey = key + }); + } + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/Lab.RefitClient.WebAPI.Caller.csproj b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/Lab.RefitClient.WebAPI.Caller.csproj new file mode 100644 index 00000000..18f67ad6 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/Lab.RefitClient.WebAPI.Caller.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/Program.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/Program.cs new file mode 100644 index 00000000..b329fb5e --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/Program.cs @@ -0,0 +1,58 @@ +using Lab.RefitClient; +using Lab.RefitClient.GeneratedCode.PetStore; +using Lab.RefitClient.WebAPI; +using Refit; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(p => p.Filters.Add()); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddSingleton>(); +builder.Services.AddSingleton>(p => p.GetService>()); +builder.Services.AddSingleton>(p => p.GetService>()); + +var baseUrl = "https://localhost:7285/api/v3"; + +builder.Services.AddSingleton(p => +{ + var settings = new RefitSettings + { + HttpMessageHandlerFactory = () => new DefaultHeaderHandler(p.GetService>()) + { + InnerHandler = new SocketsHttpHandler() + }, + }; + return settings; +}); + +builder.Services + .AddRefitClient(p => p.GetRequiredService()) + .ConfigureHttpClient(p => { p.BaseAddress = new Uri(baseUrl); }) + ; + +// builder.Services +// .AddRefitClient() +// .ConfigureHttpClient(p => { p.BaseAddress = new Uri(baseUrl); }) +// .AddHttpMessageHandler(p => new DefaultHeaderHandler(p.GetService>())) +// ; +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.RefitClient/Lab.RefitClient.WebAPI.Caller/Properties/launchSettings.json b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/Properties/launchSettings.json new file mode 100644 index 00000000..d4f149c1 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:33803", + "sslPort": 44384 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5217", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7237;http://localhost:5217", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/appsettings.Development.json b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/appsettings.json b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI.Caller/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Controllers/AutoGenerated/PetStoreController.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Controllers/AutoGenerated/PetStoreController.cs new file mode 100644 index 00000000..918daafc --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Controllers/AutoGenerated/PetStoreController.cs @@ -0,0 +1,932 @@ +//---------------------- +// +// Generated using the NSwag toolchain v13.19.0.0 (NJsonSchema v10.9.0.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 612 // Disable "CS0612 '...' is obsolete" +#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" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" + +namespace Lab.RefitClient.WebAPI.Controllers +{ + using System = global::System; + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public interface IPetStoreController + { + + /// + /// Update an existing pet + /// + + /// + /// Update an existing pet by Id + /// + + /// Update an existent pet in the store + + /// Successful operation + + System.Threading.Tasks.Task> UpdatePetAsync(Pet body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Add a new pet to the store + /// + + /// + /// Add a new pet to the store + /// + + /// Create a new pet in the store + + /// Successful operation + + System.Threading.Tasks.Task> AddPetAsync(Pet body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Finds Pets by status + /// + + /// + /// Multiple status values can be provided with comma separated strings + /// + + /// Status values that need to be considered for filter + + /// successful operation + + System.Threading.Tasks.Task>> FindPetsByStatusAsync(Status status, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Finds Pets by tags + /// + + /// + /// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + /// + + /// Tags to filter by + + /// successful operation + + System.Threading.Tasks.Task>> FindPetsByTagsAsync(System.Collections.Generic.IEnumerable tags, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Find pet by ID + /// + + /// + /// Returns a single pet + /// + + /// ID of pet to return + + /// successful operation + + System.Threading.Tasks.Task> GetPetByIdAsync(long petId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Updates a pet in the store with form data + /// + + /// ID of pet that needs to be updated + + /// Name of pet that needs to be updated + + /// Status of pet that needs to be updated + + System.Threading.Tasks.Task UpdatePetWithFormAsync(long petId, string name, string status, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Deletes a pet + /// + + + /// Pet id to delete + + System.Threading.Tasks.Task DeletePetAsync(string api_key, long petId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// uploads an image + /// + + /// ID of pet to update + + /// Additional Metadata + + + /// successful operation + + System.Threading.Tasks.Task> UploadFileAsync(long petId, string additionalMetadata, Microsoft.AspNetCore.Http.IFormFile body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Returns pet inventories by status + /// + + /// + /// Returns a map of status codes to quantities + /// + + /// successful operation + + System.Threading.Tasks.Task>> GetInventoryAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Place an order for a pet + /// + + /// + /// Place a new order in the store + /// + + /// successful operation + + System.Threading.Tasks.Task> PlaceOrderAsync(Order body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Find purchase order by ID + /// + + /// + /// For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. + /// + + /// ID of order that needs to be fetched + + /// successful operation + + System.Threading.Tasks.Task> GetOrderByIdAsync(long orderId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Delete purchase order by ID + /// + + /// + /// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + /// + + /// ID of the order that needs to be deleted + + System.Threading.Tasks.Task DeleteOrderAsync(long orderId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Create user + /// + + /// + /// This can only be done by the logged in user. + /// + + /// Created user object + + /// successful operation + + System.Threading.Tasks.Task> CreateUserAsync(User body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Creates list of users with given input array + /// + + /// + /// Creates list of users with given input array + /// + + /// Successful operation + + System.Threading.Tasks.Task> CreateUsersWithListInputAsync(System.Collections.Generic.IEnumerable body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Logs user into the system + /// + + /// The user name for login + + /// The password for login in clear text + + /// successful operation + + System.Threading.Tasks.Task> LoginUserAsync(string username, string password, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Logs out current logged in user session + /// + + /// successful operation + + System.Threading.Tasks.Task LogoutUserAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Get user by user name + /// + + /// The name that needs to be fetched. Use user1 for testing. + + /// successful operation + + System.Threading.Tasks.Task> GetUserByNameAsync(string username, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Update user + /// + + /// + /// This can only be done by the logged in user. + /// + + /// name that need to be deleted + + /// Update an existent user in the store + + /// successful operation + + System.Threading.Tasks.Task UpdateUserAsync(string username, User body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// Delete user + /// + + /// + /// This can only be done by the logged in user. + /// + + /// The name that needs to be deleted + + System.Threading.Tasks.Task DeleteUserAsync(string username, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + [Microsoft.AspNetCore.Mvc.Route("api/v3")] + + public partial class PetStoreController : Microsoft.AspNetCore.Mvc.ControllerBase + { + private IPetStoreController _implementation; + + public PetStoreController(IPetStoreController implementation) + { + _implementation = implementation; + } + + /// + /// Update an existing pet + /// + /// + /// Update an existing pet by Id + /// + /// Update an existent pet in the store + /// Successful operation + [Microsoft.AspNetCore.Mvc.HttpPut, Microsoft.AspNetCore.Mvc.Route("pet")] + public System.Threading.Tasks.Task> UpdatePet([Microsoft.AspNetCore.Mvc.FromBody] Pet body, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.UpdatePetAsync(body, cancellationToken); + } + + /// + /// Add a new pet to the store + /// + /// + /// Add a new pet to the store + /// + /// Create a new pet in the store + /// Successful operation + [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("pet")] + public System.Threading.Tasks.Task> AddPet([Microsoft.AspNetCore.Mvc.FromBody] Pet body, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.AddPetAsync(body, cancellationToken); + } + + /// + /// Finds Pets by status + /// + /// + /// Multiple status values can be provided with comma separated strings + /// + /// Status values that need to be considered for filter + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("pet/findByStatus")] + public System.Threading.Tasks.Task>> FindPetsByStatus([Microsoft.AspNetCore.Mvc.FromQuery] Status? status, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.FindPetsByStatusAsync(status ?? Lab.RefitClient.WebAPI.Controllers.Status.Available, cancellationToken); + } + + /// + /// Finds Pets by tags + /// + /// + /// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + /// + /// Tags to filter by + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("pet/findByTags")] + public System.Threading.Tasks.Task>> FindPetsByTags([Microsoft.AspNetCore.Mvc.FromQuery] System.Collections.Generic.IEnumerable tags, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.FindPetsByTagsAsync(tags, cancellationToken); + } + + /// + /// Find pet by ID + /// + /// + /// Returns a single pet + /// + /// ID of pet to return + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("pet/{petId}")] + public System.Threading.Tasks.Task> GetPetById(long petId, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.GetPetByIdAsync(petId, cancellationToken); + } + + /// + /// Updates a pet in the store with form data + /// + /// ID of pet that needs to be updated + /// Name of pet that needs to be updated + /// Status of pet that needs to be updated + [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("pet/{petId}")] + public System.Threading.Tasks.Task UpdatePetWithForm(long petId, [Microsoft.AspNetCore.Mvc.FromQuery] string name, [Microsoft.AspNetCore.Mvc.FromQuery] string status, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.UpdatePetWithFormAsync(petId, name, status, cancellationToken); + } + + /// + /// Deletes a pet + /// + /// Pet id to delete + [Microsoft.AspNetCore.Mvc.HttpDelete, Microsoft.AspNetCore.Mvc.Route("pet/{petId}")] + public System.Threading.Tasks.Task DeletePet([Microsoft.AspNetCore.Mvc.FromHeader] string api_key, long petId, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.DeletePetAsync(api_key, petId, cancellationToken); + } + + /// + /// uploads an image + /// + /// ID of pet to update + /// Additional Metadata + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("pet/{petId}/uploadImage")] + public System.Threading.Tasks.Task> UploadFile(long petId, [Microsoft.AspNetCore.Mvc.FromQuery] string additionalMetadata, Microsoft.AspNetCore.Http.IFormFile body, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.UploadFileAsync(petId, additionalMetadata, body, cancellationToken); + } + + /// + /// Returns pet inventories by status + /// + /// + /// Returns a map of status codes to quantities + /// + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("store/inventory")] + public System.Threading.Tasks.Task>> GetInventory(System.Threading.CancellationToken cancellationToken) + { + + return _implementation.GetInventoryAsync(cancellationToken); + } + + /// + /// Place an order for a pet + /// + /// + /// Place a new order in the store + /// + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("store/order")] + public System.Threading.Tasks.Task> PlaceOrder([Microsoft.AspNetCore.Mvc.FromBody] Order body, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.PlaceOrderAsync(body, cancellationToken); + } + + /// + /// Find purchase order by ID + /// + /// + /// For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. + /// + /// ID of order that needs to be fetched + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("store/order/{orderId}")] + public System.Threading.Tasks.Task> GetOrderById(long orderId, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.GetOrderByIdAsync(orderId, cancellationToken); + } + + /// + /// Delete purchase order by ID + /// + /// + /// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + /// + /// ID of the order that needs to be deleted + [Microsoft.AspNetCore.Mvc.HttpDelete, Microsoft.AspNetCore.Mvc.Route("store/order/{orderId}")] + public System.Threading.Tasks.Task DeleteOrder(long orderId, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.DeleteOrderAsync(orderId, cancellationToken); + } + + /// + /// Create user + /// + /// + /// This can only be done by the logged in user. + /// + /// Created user object + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("user")] + public System.Threading.Tasks.Task> CreateUser([Microsoft.AspNetCore.Mvc.FromBody] User body, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.CreateUserAsync(body, cancellationToken); + } + + /// + /// Creates list of users with given input array + /// + /// + /// Creates list of users with given input array + /// + /// Successful operation + [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("user/createWithList")] + public System.Threading.Tasks.Task> CreateUsersWithListInput([Microsoft.AspNetCore.Mvc.FromBody] System.Collections.Generic.IEnumerable body, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.CreateUsersWithListInputAsync(body, cancellationToken); + } + + /// + /// Logs user into the system + /// + /// The user name for login + /// The password for login in clear text + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("user/login")] + public System.Threading.Tasks.Task> LoginUser([Microsoft.AspNetCore.Mvc.FromQuery] string username, [Microsoft.AspNetCore.Mvc.FromQuery] string password, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.LoginUserAsync(username, password, cancellationToken); + } + + /// + /// Logs out current logged in user session + /// + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("user/logout")] + public System.Threading.Tasks.Task LogoutUser(System.Threading.CancellationToken cancellationToken) + { + + return _implementation.LogoutUserAsync(cancellationToken); + } + + /// + /// Get user by user name + /// + /// The name that needs to be fetched. Use user1 for testing. + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("user/{username}")] + public System.Threading.Tasks.Task> GetUserByName(string username, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.GetUserByNameAsync(username, cancellationToken); + } + + /// + /// Update user + /// + /// + /// This can only be done by the logged in user. + /// + /// name that need to be deleted + /// Update an existent user in the store + /// successful operation + [Microsoft.AspNetCore.Mvc.HttpPut, Microsoft.AspNetCore.Mvc.Route("user/{username}")] + public System.Threading.Tasks.Task UpdateUser(string username, [Microsoft.AspNetCore.Mvc.FromBody] User body, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.UpdateUserAsync(username, body, cancellationToken); + } + + /// + /// Delete user + /// + /// + /// This can only be done by the logged in user. + /// + /// The name that needs to be deleted + [Microsoft.AspNetCore.Mvc.HttpDelete, Microsoft.AspNetCore.Mvc.Route("user/{username}")] + public System.Threading.Tasks.Task DeleteUser(string username, System.Threading.CancellationToken cancellationToken) + { + + return _implementation.DeleteUserAsync(username, cancellationToken); + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class Order + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("petId")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public long PetId { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("quantity")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public int Quantity { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("shipDate")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public System.DateTimeOffset ShipDate { get; set; } + + /// + /// Order Status + /// + + [System.Text.Json.Serialization.JsonPropertyName("status")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public OrderStatus Status { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("complete")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public bool Complete { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class Customer + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("username")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string Username { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("address")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public System.Collections.Generic.List
Address { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class Address + { + + [System.Text.Json.Serialization.JsonPropertyName("street")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string Street { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("city")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string City { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("state")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string State { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("zip")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string Zip { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class Category + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("name")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string Name { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class User + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("username")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string Username { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("firstName")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string FirstName { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("lastName")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string LastName { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("email")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string Email { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("password")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string Password { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("phone")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string Phone { get; set; } + + /// + /// User Status + /// + + [System.Text.Json.Serialization.JsonPropertyName("userStatus")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public int UserStatus { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class Tag + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("name")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string Name { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class Pet + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("name")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.Never)] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Name { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("category")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public Category Category { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("photoUrls")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.Never)] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.List PhotoUrls { get; set; } = new System.Collections.Generic.List(); + + [System.Text.Json.Serialization.JsonPropertyName("tags")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public System.Collections.Generic.List Tags { get; set; } + + /// + /// pet status in the store + /// + + [System.Text.Json.Serialization.JsonPropertyName("status")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public PetStatus Status { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class ApiResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("code")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public int Code { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("type")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string Type { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("message")] + + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public string Message { get; set; } + + private System.Collections.Generic.IDictionary _additionalProperties; + + [System.Text.Json.Serialization.JsonExtensionData] + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public enum Status + { + + [System.Runtime.Serialization.EnumMember(Value = @"available")] + Available = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"pending")] + Pending = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"sold")] + Sold = 2, + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public enum OrderStatus + { + + [System.Runtime.Serialization.EnumMember(Value = @"placed")] + Placed = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"approved")] + Approved = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"delivered")] + Delivered = 2, + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public enum PetStatus + { + + [System.Runtime.Serialization.EnumMember(Value = @"available")] + Available = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"pending")] + Pending = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"sold")] + Sold = 2, + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v11.0.0.0))")] + public partial class FileParameter + { + public FileParameter(System.IO.Stream data) + : this (data, null, null) + { + } + + public FileParameter(System.IO.Stream data, string fileName) + : this (data, fileName, null) + { + } + + public FileParameter(System.IO.Stream data, string fileName, string contentType) + { + Data = data; + FileName = fileName; + ContentType = contentType; + } + + public System.IO.Stream Data { get; private set; } + + public string FileName { get; private set; } + + public string ContentType { get; private set; } + } + + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Controllers/PetstoreControllerImpl.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Controllers/PetstoreControllerImpl.cs new file mode 100644 index 00000000..32777006 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Controllers/PetstoreControllerImpl.cs @@ -0,0 +1,130 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Lab.RefitClient.WebAPI.Controllers; + +public class PetStoreControllerImpl : IPetStoreController +{ + private readonly IContextGetter _contextGetter; + private readonly IHttpContextAccessor _httpContextAccessor; + + public PetStoreControllerImpl(IContextGetter contextGetter, + IHttpContextAccessor httpContextAccessor) + { + this._contextGetter = contextGetter; + this._httpContextAccessor = httpContextAccessor; + } + + public Task> UpdatePetAsync(Pet body, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> AddPetAsync(Pet body, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task>> FindPetsByStatusAsync(Status status, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task>> FindPetsByTagsAsync(IEnumerable tags, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> GetPetByIdAsync(long petId, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task UpdatePetWithFormAsync(long petId, string name, string status, + CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task DeletePetAsync(string api_key, long petId, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> UploadFileAsync(long petId, string additionalMetadata, IFormFile body, + CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task>> GetInventoryAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> PlaceOrderAsync(Order body, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> GetOrderByIdAsync(long orderId, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task DeleteOrderAsync(long orderId, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> CreateUserAsync(User body, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> CreateUsersWithListInputAsync(IEnumerable body, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> LoginUserAsync(string username, string password, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task LogoutUserAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public async Task> GetUserByNameAsync(string username, CancellationToken cancellationToken = default(CancellationToken)) + { + // 透過一個 Filter 處理 Header 並轉呈 HeaderContext 物件 + var headerContext = this._contextGetter.Get(); + + var response = this._httpContextAccessor.HttpContext.Response; + response.Headers.Add(PetStoreHeaderNames.IdempotencyKey,headerContext.IdempotencyKey); + response.Headers.Add(PetStoreHeaderNames.ApiKey,headerContext.ApiKey); + return new User + { + Id = 0, + Username = username, + FirstName = null, + LastName = null, + Email = "yao@aa.bb", + Password = null, + Phone = null, + UserStatus = 0, + AdditionalProperties = null + }; + } + + public Task UpdateUserAsync(string username, User body, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task DeleteUserAsync(string username, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Lab.RefitClient.WebAPI.csproj b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Lab.RefitClient.WebAPI.csproj new file mode 100644 index 00000000..0209724e --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Lab.RefitClient.WebAPI.csproj @@ -0,0 +1,24 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Program.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Program.cs new file mode 100644 index 00000000..e9d88138 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Program.cs @@ -0,0 +1,47 @@ +using Lab.RefitClient; +using Lab.RefitClient.WebAPI; +using Lab.RefitClient.WebAPI.Controllers; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(p => +{ + p.Filters.Add(new ResolverHeaderContextFilterAttribute()); +}); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddHttpContextAccessor(); + +builder.Services.AddSingleton>(); +builder.Services.AddSingleton>(p => p.GetService>()); +builder.Services.AddSingleton>(p => p.GetService>()); + +builder.Services.AddScoped(); + +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(); + +namespace Lab.RefitClient.WebAPI +{ + public partial class Program + { + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Properties/launchSettings.json b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Properties/launchSettings.json new file mode 100644 index 00000000..9919f9b0 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55975", + "sslPort": 44397 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5232", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7285;http://localhost:5232", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/ResolverHeaderContextFilterAttribute.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/ResolverHeaderContextFilterAttribute.cs new file mode 100644 index 00000000..b28cb8bf --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/ResolverHeaderContextFilterAttribute.cs @@ -0,0 +1,20 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Lab.RefitClient.WebAPI +{ + public class ResolverHeaderContextFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext actionContext) + { + var idempotencyKey = actionContext.HttpContext.Request.Headers[PetStoreHeaderNames.IdempotencyKey]; + var apiKey = actionContext.HttpContext.Request.Headers[PetStoreHeaderNames.ApiKey]; + var headerContext = actionContext.HttpContext.RequestServices.GetService>(); + headerContext.Set(new HeaderContext + { + IdempotencyKey = idempotencyKey, + ApiKey = apiKey + }); + } + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/appsettings.Development.json b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/appsettings.json b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.WebAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.sln b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.sln new file mode 100644 index 00000000..dcfa3ce8 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.RefitClient.WebAPI", "Lab.RefitClient.WebAPI\Lab.RefitClient.WebAPI.csproj", "{A327EA7D-1FA9-4CA0-A879-612D5DD6476D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.RefitClient.TestProject", "Lab.RefitClient.TestProject\Lab.RefitClient.TestProject.csproj", "{41937C8E-4831-4DA2-AA12-419C5727A2D8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.RefitClient", "Lab.RefitClient\Lab.RefitClient.csproj", "{B0351748-0968-4F05-92F5-EF1C58D7CC3D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.RefitClient.WebAPI.Caller", "Lab.RefitClient.WebAPI.Caller\Lab.RefitClient.WebAPI.Caller.csproj", "{D6628998-3E70-4747-8A02-F4FA53D000B9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A327EA7D-1FA9-4CA0-A879-612D5DD6476D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A327EA7D-1FA9-4CA0-A879-612D5DD6476D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A327EA7D-1FA9-4CA0-A879-612D5DD6476D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A327EA7D-1FA9-4CA0-A879-612D5DD6476D}.Release|Any CPU.Build.0 = Release|Any CPU + {41937C8E-4831-4DA2-AA12-419C5727A2D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41937C8E-4831-4DA2-AA12-419C5727A2D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41937C8E-4831-4DA2-AA12-419C5727A2D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41937C8E-4831-4DA2-AA12-419C5727A2D8}.Release|Any CPU.Build.0 = Release|Any CPU + {B0351748-0968-4F05-92F5-EF1C58D7CC3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0351748-0968-4F05-92F5-EF1C58D7CC3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0351748-0968-4F05-92F5-EF1C58D7CC3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0351748-0968-4F05-92F5-EF1C58D7CC3D}.Release|Any CPU.Build.0 = Release|Any CPU + {D6628998-3E70-4747-8A02-F4FA53D000B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6628998-3E70-4747-8A02-F4FA53D000B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6628998-3E70-4747-8A02-F4FA53D000B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6628998-3E70-4747-8A02-F4FA53D000B9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/ContextAccessor.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/ContextAccessor.cs new file mode 100644 index 00000000..fff185dc --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/ContextAccessor.cs @@ -0,0 +1,22 @@ +namespace Lab.RefitClient +{ + public class ContextAccessor : IContextSetter, IContextGetter where T : class + { + private static readonly AsyncLocal s_current = new AsyncLocal(); + + public T Get() + { + return s_current?.Value; + } + + public void Set(T value) + { + if (s_current == null) + { + return; + } + + s_current.Value = value; + } + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/DefaultHeaderHandler.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/DefaultHeaderHandler.cs new file mode 100644 index 00000000..294ff059 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/DefaultHeaderHandler.cs @@ -0,0 +1,21 @@ +namespace Lab.RefitClient; + +public class DefaultHeaderHandler : DelegatingHandler +{ + private readonly IContextGetter _contextGetter; + + public DefaultHeaderHandler(IContextGetter contextGetter) + { + this._contextGetter = contextGetter; + } + + protected override async Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + var headerContext = this._contextGetter.Get(); + request.Headers.Add(PetStoreHeaderNames.IdempotencyKey, headerContext.IdempotencyKey); + request.Headers.Add(PetStoreHeaderNames.ApiKey, headerContext.ApiKey); + + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/HeaderContext.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/HeaderContext.cs new file mode 100644 index 00000000..62129d7d --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/HeaderContext.cs @@ -0,0 +1,8 @@ +namespace Lab.RefitClient; + +public class HeaderContext +{ + public string IdempotencyKey { get; set; } + + public string ApiKey { get; set; } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/IContextGetter.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/IContextGetter.cs new file mode 100644 index 00000000..9f5d5fc1 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/IContextGetter.cs @@ -0,0 +1,7 @@ +namespace Lab.RefitClient +{ + public interface IContextGetter where T : class + { + T Get(); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/IContextSetter.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/IContextSetter.cs new file mode 100644 index 00000000..a6356ce4 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/IContextSetter.cs @@ -0,0 +1,7 @@ +namespace Lab.RefitClient +{ + public interface IContextSetter where T : class + { + void Set(T trackContext); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/Lab.RefitClient.csproj b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/Lab.RefitClient.csproj new file mode 100644 index 00000000..b079d69a --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/Lab.RefitClient.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/PetStoreClient.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/PetStoreClient.cs new file mode 100644 index 00000000..1efb7c1f --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/PetStoreClient.cs @@ -0,0 +1,542 @@ +// +// This code was generated by Refitter. +// + +using Refit; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Lab.RefitClient.GeneratedCode.PetStore +{ + [System.CodeDom.Compiler.GeneratedCode("Refitter", "0.7.3.0")] + public interface ISwaggerPetstoreOpenAPI30 + { + /// + /// Update an existing pet by Id + /// + [Headers("Accept: application/xml, application/json")] + [Put("/pet")] + Task> UpdatePet([Body] Pet body); + + /// + /// Add a new pet to the store + /// + [Headers("Accept: application/xml, application/json")] + [Post("/pet")] + Task> AddPet([Body] Pet body); + + /// + /// Multiple status values can be provided with comma separated strings + /// + [Headers("Accept: application/xml, application/json")] + [Get("/pet/findByStatus")] + Task>> FindPetsByStatus([Query] Status? status); + + /// + /// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + /// + [Headers("Accept: application/xml, application/json")] + [Get("/pet/findByTags")] + Task>> FindPetsByTags([Query(CollectionFormat.Multi)] IEnumerable tags); + + /// + /// Returns a single pet + /// + [Headers("Accept: application/xml, application/json")] + [Get("/pet/{petId}")] + Task> GetPetById(long petId); + + [Post("/pet/{petId}")] + Task UpdatePetWithForm(long petId, [Query] string name, [Query] string status); + + [Delete("/pet/{petId}")] + Task DeletePet(long petId); + + [Headers("Accept: application/json")] + [Post("/pet/{petId}/uploadImage")] + Task> UploadFile(long petId, [Query] string additionalMetadata, StreamPart body); + + /// + /// Returns a map of status codes to quantities + /// + [Headers("Accept: application/json")] + [Get("/store/inventory")] + Task>> GetInventory(); + + /// + /// Place a new order in the store + /// + [Headers("Accept: application/json")] + [Post("/store/order")] + Task> PlaceOrder([Body] Order body); + + /// + /// For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. + /// + [Headers("Accept: application/xml, application/json")] + [Get("/store/order/{orderId}")] + Task> GetOrderById(long orderId); + + /// + /// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + /// + [Delete("/store/order/{orderId}")] + Task DeleteOrder(long orderId); + + /// + /// This can only be done by the logged in user. + /// + [Headers("Accept: application/json, application/xml")] + [Post("/user")] + Task CreateUser([Body] User body); + + /// + /// Creates list of users with given input array + /// + [Headers("Accept: application/xml, application/json")] + [Post("/user/createWithList")] + Task> CreateUsersWithListInput([Body] IEnumerable body); + + [Headers("Accept: application/xml, application/json")] + [Get("/user/login")] + Task> LoginUser([Query] string username, [Query] string password); + + [Get("/user/logout")] + Task LogoutUser(); + + [Headers("Accept: application/xml, application/json")] + [Get("/user/{username}")] + Task> GetUserByName(string username); + + /// + /// This can only be done by the logged in user. + /// + [Put("/user/{username}")] + Task UpdateUser(string username, [Body] User body); + + /// + /// This can only be done by the logged in user. + /// + [Delete("/user/{username}")] + Task DeleteUser(string username); + + + } +} + + +//---------------------- +// +// Generated using the NSwag toolchain v13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.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 612 // Disable "CS0612 '...' is obsolete" +#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" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" + +namespace Lab.RefitClient.GeneratedCode.PetStore +{ + using System = global::System; + + + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class Order + { + + [JsonPropertyName("id")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [JsonPropertyName("petId")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long PetId { get; set; } + + [JsonPropertyName("quantity")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Quantity { get; set; } + + [JsonPropertyName("shipDate")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public System.DateTimeOffset ShipDate { get; set; } + + /// + /// Order Status + /// + + [JsonPropertyName("status")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonConverter(typeof(JsonStringEnumConverter))] + public OrderStatus Status { get; set; } + + [JsonPropertyName("complete")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool Complete { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class Customer + { + + [JsonPropertyName("id")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [JsonPropertyName("username")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Username { get; set; } + + [JsonPropertyName("address")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ICollection
Address { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class Address + { + + [JsonPropertyName("street")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Street { get; set; } + + [JsonPropertyName("city")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string City { get; set; } + + [JsonPropertyName("state")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string State { get; set; } + + [JsonPropertyName("zip")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Zip { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class Category + { + + [JsonPropertyName("id")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [JsonPropertyName("name")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Name { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class User + { + + [JsonPropertyName("id")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [JsonPropertyName("username")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Username { get; set; } + + [JsonPropertyName("firstName")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string FirstName { get; set; } + + [JsonPropertyName("lastName")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string LastName { get; set; } + + [JsonPropertyName("email")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Email { get; set; } + + [JsonPropertyName("password")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Password { get; set; } + + [JsonPropertyName("phone")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Phone { get; set; } + + /// + /// User Status + /// + + [JsonPropertyName("userStatus")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int UserStatus { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class Tag + { + + [JsonPropertyName("id")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [JsonPropertyName("name")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Name { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class Pet + { + + [JsonPropertyName("id")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Id { get; set; } + + [JsonPropertyName("name")] + + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Name { get; set; } + + [JsonPropertyName("category")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Category Category { get; set; } + + [JsonPropertyName("photoUrls")] + + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + [System.ComponentModel.DataAnnotations.Required] + public ICollection PhotoUrls { get; set; } = new System.Collections.ObjectModel.Collection(); + + [JsonPropertyName("tags")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ICollection Tags { get; set; } + + /// + /// pet status in the store + /// + + [JsonPropertyName("status")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonConverter(typeof(JsonStringEnumConverter))] + public PetStatus Status { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class ApiResponse + { + + [JsonPropertyName("code")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Code { get; set; } + + [JsonPropertyName("type")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Type { get; set; } + + [JsonPropertyName("message")] + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Message { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public enum Status + { + + [System.Runtime.Serialization.EnumMember(Value = @"available")] + Available = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"pending")] + Pending = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"sold")] + Sold = 2, + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public enum OrderStatus + { + + [System.Runtime.Serialization.EnumMember(Value = @"placed")] + Placed = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"approved")] + Approved = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"delivered")] + Delivered = 2, + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public enum PetStatus + { + + [System.Runtime.Serialization.EnumMember(Value = @"available")] + Available = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"pending")] + Pending = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"sold")] + Sold = 2, + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class FileParameter + { + public FileParameter(System.IO.Stream data) + : this (data, null, null) + { + } + + public FileParameter(System.IO.Stream data, string fileName) + : this (data, fileName, null) + { + } + + public FileParameter(System.IO.Stream data, string fileName, string contentType) + { + Data = data; + FileName = fileName; + ContentType = contentType; + } + + public System.IO.Stream Data { get; private set; } + + public string FileName { get; private set; } + + public string ContentType { get; private set; } + } + + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 +#pragma warning restore 8604 diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/PetStoreHeaderNames.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/PetStoreHeaderNames.cs new file mode 100644 index 00000000..4f44edba --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/PetStoreHeaderNames.cs @@ -0,0 +1,7 @@ +namespace Lab.RefitClient; + +public class PetStoreHeaderNames +{ + public const string IdempotencyKey = "x-idempotency-key"; + public const string ApiKey = "x-api-key"; +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/Workflow.cs b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/Workflow.cs new file mode 100644 index 00000000..1dc0f8cf --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Lab.RefitClient/Workflow.cs @@ -0,0 +1,19 @@ +using Lab.RefitClient.GeneratedCode.PetStore; + +namespace Lab.RefitClient; + +public class Workflow +{ + readonly ISwaggerPetstoreOpenAPI30 _petStore; + + public Workflow(ISwaggerPetstoreOpenAPI30 petStore) + { + this._petStore = petStore; + } + + public async Task GetUser(string name) + { + var getUserByNameResult = await this._petStore.GetUserByName(name); + return getUserByNameResult.Content.Username; + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Lab.RefitClient/Taskfile.yml b/WebAPI/Swagger/Lab.RefitClient/Taskfile.yml new file mode 100644 index 00000000..b4927c07 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/Taskfile.yml @@ -0,0 +1,19 @@ +# Taskfile.yml + +version: "3" + +tasks: + codegen: + desc: codegen client and server + cmds: + - task: codegen-client + - task: codegen-server + codegen-client: + desc: codegen client + cmds: +# - refitter ./openapi.json --namespace "Lab.RefitClient.GeneratedCode.PetStore" --output ./Lab.RefitClient/PetStoreClient.cs --use-api-response --no-operation-headers + - refitter ./openapi.json --namespace "Lab.RefitClient.GeneratedCode.PetStore" --output ./Lab.RefitClient/PetStoreClient.cs --use-api-response --no-operation-headers --no-auto-generated-header + codegen-server: + desc: codegen server + cmds: + - nswag openapi2cscontroller /input:./openapi.json /classname:PetStore /namespace:Lab.RefitClient.WebAPI.Controllers /output:Lab.RefitClient.WebAPI/Controllers/AutoGenerated/PetStoreController.cs /jsonLibrary:SystemTextJson /useCancellationToken:true /useActionResultType:true /excludedParameterNames:x-idempotency-key,x-api-key diff --git a/WebAPI/Swagger/Lab.RefitClient/openapi.json b/WebAPI/Swagger/Lab.RefitClient/openapi.json new file mode 100644 index 00000000..80ffe3e2 --- /dev/null +++ b/WebAPI/Swagger/Lab.RefitClient/openapi.json @@ -0,0 +1,1243 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Swagger Petstore - OpenAPI 3.0", + "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.17" + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + }, + "servers": [ + { + "url": "/api/v3" + } + ], + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + }, + { + "name": "user", + "description": "Operations about user" + } + ], + "paths": { + "/pet": { + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "Update an existing pet by Id", + "operationId": "updatePet", + "requestBody": { + "description": "Update an existent pet in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "addPet", + "requestBody": { + "description": "Create a new pet in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": false, + "explode": true, + "schema": { + "type": "string", + "default": "available", + "enum": [ + "available", + "pending", + "sold" + ] + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": false, + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "api_key": [] + }, + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "name", + "in": "query", + "description": "Name of pet that needs to be updated", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "description": "Status of pet that needs to be updated", + "schema": { + "type": "string" + } + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "parameters": [ + { + "name": "api_key", + "in": "header", + "description": "", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { + "description": "Invalid pet value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "additionalMetadata", + "in": "query", + "description": "Additional Metadata", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "Place a new order in the store", + "operationId": "placeOrder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "405": { + "description": "Invalid input" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", + "operationId": "getOrderById", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of order that needs to be fetched", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", + "operationId": "deleteOrder", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "requestBody": { + "description": "Created user object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "responses": { + "default": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "Creates list of users with given input array", + "operationId": "createUsersWithListInput", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "headers": { + "X-Rate-Limit": { + "description": "calls per hour allowed by the user", + "schema": { + "type": "integer", + "format": "int32" + } + }, + "X-Expires-After": { + "description": "date in UTC when token expires", + "schema": { + "type": "string", + "format": "date-time" + } + } + }, + "content": { + "application/xml": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "parameters": [], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "x-idempotency-key", + "in": "header", + "description": "idempotency key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "x-api-key", + "in": "header", + "description": "api key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be deleted", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Update an existent user in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "responses": { + "default": { + "description": "successful operation" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "components": { + "schemas": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "petId": { + "type": "integer", + "format": "int64", + "example": 198772 + }, + "quantity": { + "type": "integer", + "format": "int32", + "example": 7 + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "example": "approved", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean" + } + }, + "xml": { + "name": "order" + } + }, + "Customer": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 100000 + }, + "username": { + "type": "string", + "example": "fehguy" + }, + "address": { + "type": "array", + "xml": { + "name": "addresses", + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Address" + } + } + }, + "xml": { + "name": "customer" + } + }, + "Address": { + "type": "object", + "properties": { + "street": { + "type": "string", + "example": "437 Lytton" + }, + "city": { + "type": "string", + "example": "Palo Alto" + }, + "state": { + "type": "string", + "example": "CA" + }, + "zip": { + "type": "string", + "example": "94301" + } + }, + "xml": { + "name": "address" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1 + }, + "name": { + "type": "string", + "example": "Dogs" + } + }, + "xml": { + "name": "category" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "username": { + "type": "string", + "example": "theUser" + }, + "firstName": { + "type": "string", + "example": "John" + }, + "lastName": { + "type": "string", + "example": "James" + }, + "email": { + "type": "string", + "example": "john@email.com" + }, + "password": { + "type": "string", + "example": "12345" + }, + "phone": { + "type": "string", + "example": "12345" + }, + "userStatus": { + "type": "integer", + "description": "User Status", + "format": "int32", + "example": 1 + } + }, + "xml": { + "name": "user" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "tag" + } + }, + "Pet": { + "required": [ + "name", + "photoUrls" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "name": { + "type": "string", + "example": "doggie" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "photoUrls": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "type": "string", + "xml": { + "name": "photoUrl" + } + } + }, + "tags": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "xml": { + "name": "##default" + } + } + }, + "requestBodies": { + "Pet": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "UserArray": { + "description": "List of user object", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + }, + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + } + } +} \ No newline at end of file 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/Lab.MockServer/.gitignore b/WebAPI/Swagger/Mock Server/Lab.MockServer/.gitignore new file mode 100644 index 00000000..f2c4b13c --- /dev/null +++ b/WebAPI/Swagger/Mock Server/Lab.MockServer/.gitignore @@ -0,0 +1,670 @@ +### ASPNETCore template +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# 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 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# 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 + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.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 + +# 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 + +# 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 +# TODO: 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 +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable 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 + +# 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 +node_modules/ +orleans.codegen.cs + +# 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 + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# 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/ + +### Csharp 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/main/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 +*.tlog +*.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 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# 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/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# 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 + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + diff --git a/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/GlobalUsings.cs b/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/GlobalUsings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/Lab.MockServer.Test.csproj b/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/Lab.MockServer.Test.csproj new file mode 100644 index 00000000..f21206f7 --- /dev/null +++ b/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/Lab.MockServer.Test.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + Linux + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/UnitTest1.cs b/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/UnitTest1.cs new file mode 100644 index 00000000..a6bff948 --- /dev/null +++ b/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/UnitTest1.cs @@ -0,0 +1,195 @@ +using System.Net; +using System.Text; +using System.Text.Json.JsonDiffPatch; +using FluentAssertions; + +namespace Lab.MockServer.Test; + +public class UnitTest1 +{ + static readonly HttpClient Client = new() + { + BaseAddress = new Uri("http://localhost:1080/"), + }; + + [Fact] + public void 動態建立假端點() + { + //建立假的端點 + var url = "mockserver/expectation"; + var body = """ + { + "httpRequest": { + "method": "GET", + "path": "/view/cart" + }, + "httpResponse": { + "body": "some_response_body" + } + } + """; + var request = new HttpRequestMessage(HttpMethod.Put, url); + request.Content = new StringContent(body, Encoding.UTF8, "application/json"); + var response = Client.SendAsync(request).Result; + response.StatusCode.Should().Be(HttpStatusCode.Created); + + //呼叫假的端點 + var getCartResult = Client.GetStringAsync("view/cart?cartId=055CA455-1DF7-45BB-8535-4F83E7266092").Result; + getCartResult.Should().Be("some_response_body"); + } + + [Fact] + public void 匯入OpenApi() + { + //建立假的端點 + var url = "mockserver/openapi"; + + var yaml = @" +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 + maximum: 100 + 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 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + required: true + 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 + maxItems: 100 + items: + $ref: '#/components/schemas/Pet' + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string"; + + var httpFile = "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml"; + var jsonPayload = new + { + specUrlOrPayload = httpFile + }; + + var body = System.Text.Json.JsonSerializer.Serialize(jsonPayload); + var request = new HttpRequestMessage(HttpMethod.Put, url); + request.Content = new StringContent(body, Encoding.UTF8, "application/json"); + var response = Client.SendAsync(request).Result; + response.StatusCode.Should().Be(HttpStatusCode.Created); + + //呼叫假的端點 + var getCartResult = Client.GetStringAsync("/v1/pets").Result; + + var expected = """ + [ + { + "id": 0, + "name": "some_string_value", + "tag": "some_string_value" + } + ] + """; + var diff = JsonDiffPatcher.Diff(expected, getCartResult); + Assert.Null(diff); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/UnitTest2.cs b/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/UnitTest2.cs new file mode 100644 index 00000000..d992816a --- /dev/null +++ b/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.Test/UnitTest2.cs @@ -0,0 +1,48 @@ +using System.Net; +using System.Text; +using System.Text.Json.JsonDiffPatch; +using DotNet.Testcontainers.Builders; +using FluentAssertions; + +namespace Lab.MockServer.Test; + +public class UnitTest2 +{ + [Fact] + public async Task 動態建立假端點_TestContainers() + { + //建立假的端點 + var url = "mockserver/expectation"; + var body = """ + { + "httpRequest": { + "method": "GET", + "path": "/view/cart" + }, + "httpResponse": { + "body": "some_response_body" + } + } + """; + + var container = new ContainerBuilder() + .WithImage("mockserver/mockserver") + .WithPortBinding(1080, assignRandomHostPort: true) + .Build(); + await container.StartAsync(); + var hostname = container.Hostname; + var port = container.GetMappedPublicPort(1080); + var httpClient = new HttpClient + { + BaseAddress = new Uri($"http://{hostname}:{port}/") + }; + var request = new HttpRequestMessage(HttpMethod.Put, url); + request.Content = new StringContent(body, Encoding.UTF8, "application/json"); + var response = httpClient.SendAsync(request).Result; + response.StatusCode.Should().Be(HttpStatusCode.Created); + + //呼叫假的端點 + var getCartResult = httpClient.GetStringAsync("view/cart?cartId=055CA455-1DF7-45BB-8535-4F83E7266092").Result; + getCartResult.Should().Be("some_response_body"); + } +} \ No newline at end of file diff --git a/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.sln b/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.sln new file mode 100644 index 00000000..3d802178 --- /dev/null +++ b/WebAPI/Swagger/Mock Server/Lab.MockServer/Lab.MockServer.sln @@ -0,0 +1,21 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lab.MockServer.Test", "Lab.MockServer.Test\Lab.MockServer.Test.csproj", "{F9475396-EE48-49F7-B36F-32A6053F31A4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EB3E2200-3D1A-4778-A98A-254F59AF0238}" + 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 + {F9475396-EE48-49F7-B36F-32A6053F31A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9475396-EE48-49F7-B36F-32A6053F31A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9475396-EE48-49F7-B36F-32A6053F31A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9475396-EE48-49F7-B36F-32A6053F31A4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebAPI/Swagger/Mock Server/Lab.MockServer/Taslfile.yml b/WebAPI/Swagger/Mock Server/Lab.MockServer/Taslfile.yml new file mode 100644 index 00000000..e69de29b diff --git a/WebAPI/Swagger/Mock Server/Lab.MockServer/docker-compose.yml b/WebAPI/Swagger/Mock Server/Lab.MockServer/docker-compose.yml new file mode 100644 index 00000000..16e62d7c --- /dev/null +++ b/WebAPI/Swagger/Mock Server/Lab.MockServer/docker-compose.yml @@ -0,0 +1,9 @@ +services: + mockServer: + image: mockserver/mockserver:latest + container_name: mockServer + ports: + - 1080:1080 +# environment: +# MOCKSERVER_MAX_EXPECTATIONS: 100 +# MOCKSERVER_MAX_HEADER_SIZE: 8192 \ No newline at end of file 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 diff --git a/WebAPI/Swagger/Redoc/Taskfile.yml b/WebAPI/Swagger/Redoc/Taskfile.yml new file mode 100644 index 00000000..b0e1e760 --- /dev/null +++ b/WebAPI/Swagger/Redoc/Taskfile.yml @@ -0,0 +1,16 @@ +# Taskfile.yml + +version: "3" + +tasks: + codegen-api-doc: + desc: codegen api doc + 安裝 Redocly CLI + npm install -g @redocly/openapi-cli + cmds: + - redocly build-docs ./petstore.yaml --output ./petstore.html + + api-preview: + desc: preview + cmds: + - redocly preview-docs ./petstore.yaml diff --git a/WebAPI/Swagger/Redoc/description.md b/WebAPI/Swagger/Redoc/description.md new file mode 100644 index 00000000..6d9d6e8f --- /dev/null +++ b/WebAPI/Swagger/Redoc/description.md @@ -0,0 +1,10 @@ +# Web API +This is the API documentation. + +## Features + +- Item1 +- Item2 + +1. Number1 +2. Number2 diff --git a/WebAPI/Swagger/Redoc/petstore.html b/WebAPI/Swagger/Redoc/petstore.html new file mode 100644 index 00000000..642387c8 --- /dev/null +++ b/WebAPI/Swagger/Redoc/petstore.html @@ -0,0 +1,415 @@ + + + + + + Swagger Petstore + + + + + + + + + +

Swagger Petstore (1.0.0)

Download OpenAPI specification:Download

License: MIT

Web API

This is the API documentation.

+

Features

    +
  • Item1
  • +
  • Item2
  • +
+
    +
  1. Number1
  2. +
  3. Number2
  4. +
  5. Number2
  6. +
+

pets

List all pets

query Parameters
limit
integer <int32> <= 100

How many items to return at one time (max 100)

+

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Create a pet

Request Body schema: application/json
required
id
required
integer <int64>
name
required
string
tag
string

Responses

Request samples

Content type
application/json
{
  • "id": 0,
  • "name": "string",
  • "tag": "string"
}

Response samples

Content type
application/json
{
  • "code": 0,
  • "message": "string"
}

Info for a specific pet

path Parameters
petId
required
string

The id of the pet to retrieve

+

Responses

Response samples

Content type
application/json
{
  • "id": 0,
  • "name": "string",
  • "tag": "string"
}
+ + + + diff --git a/WebAPI/Swagger/Redoc/petstore.yaml b/WebAPI/Swagger/Redoc/petstore.yaml new file mode 100644 index 00000000..671bc2c4 --- /dev/null +++ b/WebAPI/Swagger/Redoc/petstore.yaml @@ -0,0 +1,131 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT + description: | + # Web API + This is the API documentation. + + ## Features + + - Item1 + - Item2 + + 1. Number1 + 2. Number2 + 3. Number3 +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 + maximum: 100 + 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 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + required: true + 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 + maxItems: 100 + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Context.Trace/JobBank1111.Context.Trace.csproj" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Context.Trace/JobBank1111.Context.Trace.csproj" new file mode 100644 index 00000000..ceb80a0f --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Context.Trace/JobBank1111.Context.Trace.csproj" @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + Linux + Lab.Context.Trace + + + + + + + + + diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/Failure.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/Failure.cs" new file mode 100644 index 00000000..30ddfa95 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/Failure.cs" @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace JobBank1111.Infrastructure; + +public class Failure +{ + public Failure() + { + } + + public Failure(FailureCode code, string message) + { + this.Code = code; + this.Message = message; + } + + /// + /// 錯誤碼 + /// + public FailureCode Code { get; init; } + + /// + /// 錯誤訊息 + /// + public string Message { get; init; } + + /// + /// 錯誤發生時的資料 + /// + public object Data { get; init; } + + /// + /// 追蹤 Id + /// + public string TraceId { get; set; } + + /// + /// 例外,不回傳給 Web API + /// + [JsonIgnore] + public Exception Exception { get; set; } + + public List Details { get; init; } = new(); +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/FailureCode.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/FailureCode.cs" new file mode 100644 index 00000000..6dd9d842 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/FailureCode.cs" @@ -0,0 +1,6 @@ +namespace JobBank1111.Infrastructure; + +public enum FailureCode +{ + Unauthorized +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/JobBank1111.Infrastructure.csproj" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/JobBank1111.Infrastructure.csproj" new file mode 100644 index 00000000..3a635329 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/JobBank1111.Infrastructure.csproj" @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/AuthContext.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/AuthContext.cs" new file mode 100644 index 00000000..9cb8c1cf --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/AuthContext.cs" @@ -0,0 +1,8 @@ +namespace JobBank1111.Infrastructure.TraceContext; + +public record AuthContext +{ + public string TraceId { get; init; } + + public string UserId { get; init; } +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/ContextAccessor.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/ContextAccessor.cs" new file mode 100644 index 00000000..d6da89a8 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/ContextAccessor.cs" @@ -0,0 +1,23 @@ +namespace JobBank1111.Infrastructure.TraceContext; + +public class ContextAccessor : IContextSetter, IContextGetter + where T : class +{ + private static readonly AsyncLocal> s_current = new(); + + public T? Get() + { + var contextHolder = s_current.Value; + return contextHolder?.Value; + } + + public void Set(T value) + { + if (s_current.Value == null) + { + s_current.Value = new ContextHolder(); + } + + s_current.Value.Value = value; + } +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/ContextHolder.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/ContextHolder.cs" new file mode 100644 index 00000000..9791c275 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/ContextHolder.cs" @@ -0,0 +1,6 @@ +namespace JobBank1111.Infrastructure.TraceContext; + +public class ContextHolder +{ + public T Value { get; set; } +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/IContextGetter.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/IContextGetter.cs" new file mode 100644 index 00000000..ff5225ff --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/IContextGetter.cs" @@ -0,0 +1,6 @@ +namespace JobBank1111.Infrastructure.TraceContext; + +public interface IContextGetter +{ + T? Get(); +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/IContextSetter.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/IContextSetter.cs" new file mode 100644 index 00000000..aa940de7 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Infrastructure/TraceContext/IContextSetter.cs" @@ -0,0 +1,6 @@ +namespace JobBank1111.Infrastructure.TraceContext; + +public interface IContextSetter +{ + void Set(T value); +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.Management.sln" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.Management.sln" new file mode 100644 index 00000000..d1179617 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.Management.sln" @@ -0,0 +1,33 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{47606710-3952-41C4-8B80-57FB42EF000C}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobBank1111.Job.WebAPI.Test", "JobBank1111.Job.WebAPI.Test\JobBank1111.Job.WebAPI.Test.csproj", "{072B154D-149F-416C-AC1A-E009FED7706E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobBank1111.Infrastructure", "JobBank1111.Infrastructure\JobBank1111.Infrastructure.csproj", "{F9C2045E-64DE-417A-BCC7-FE20B982153B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobBank1111.Job.WebAPI", "JobBank1111.Job.WebAPI\JobBank1111.Job.WebAPI.csproj", "{5BB4C0EB-337D-44F4-BE0A-0694CAF47890}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {072B154D-149F-416C-AC1A-E009FED7706E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {072B154D-149F-416C-AC1A-E009FED7706E}.Release|Any CPU.Build.0 = Release|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9C2045E-64DE-417A-BCC7-FE20B982153B}.Release|Any CPU.Build.0 = Release|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BB4C0EB-337D-44F4-BE0A-0694CAF47890}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/JobBank1111.Job.WebAPI.Test.csproj" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/JobBank1111.Job.WebAPI.Test.csproj" new file mode 100644 index 00000000..82d5a6fc --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/JobBank1111.Job.WebAPI.Test.csproj" @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + false + + + + + + + + + + + diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/TestServer.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/TestServer.cs" new file mode 100644 index 00000000..9b103626 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/TestServer.cs" @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestPlatform.TestHost; + +namespace JobBank1111.Job.WebAPI.Test; + +public class TestServer : WebApplicationFactory +{ + private void ConfigureServices(IServiceCollection services) + { + + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(this.ConfigureServices); + } +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/UnitTest1.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/UnitTest1.cs" new file mode 100644 index 00000000..d7063a97 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/UnitTest1.cs" @@ -0,0 +1,52 @@ +namespace JobBank1111.Job.WebAPI.Test; + +class Data +{ + public string? TraceId { get; set; } +} + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public async Task TestMethod1() + { + var server = new TestServer(); + var httpClient = server.CreateDefaultClient(); + + var url = "https://localhost:7004/demo"; + + var tasks = new List>(); + for (var i = 0; i < 10000; i++) + { + tasks.Add(SendAsync(httpClient, url)); + } + + var data = await Task.WhenAll(tasks); + + var duplicateData = data.GroupBy(p => p.TraceId) + .Where(p => p.Count() > 1) + .Select(p => p.Key); + + foreach (var item in duplicateData) + { + Console.WriteLine(item); + } + + if (duplicateData.Any()) + { + Assert.Fail("有重複的 trace id"); + } + } + + static async Task SendAsync(HttpClient httpClient, string url) + { + var response = await httpClient.GetAsync(url); + response.Headers.TryGetValues("x-trace-id", out var traceIds); + var traceId = traceIds.FirstOrDefault(); + return new Data() + { + TraceId = traceId + }; + } +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/Usings.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/Usings.cs" new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI.Test/Usings.cs" @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Controllers/DemoController.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Controllers/DemoController.cs" new file mode 100644 index 00000000..4dd8ff41 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Controllers/DemoController.cs" @@ -0,0 +1,33 @@ +using JobBank1111.Infrastructure.TraceContext; +using JobBank1111.Job.WebAPI.Models; +using Microsoft.AspNetCore.Mvc; + +namespace JobBank1111.Job.WebAPI.Controllers; + +[ApiController] +[Route("[controller]")] +public class DemoController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IContextGetter _authContextGetter; + + public DemoController(ILogger logger, + IContextGetter authContextGetter) + { + this._logger = logger; + this._authContextGetter = authContextGetter; + } + + [HttpGet(Name = "GetDemo")] + public ActionResult Get() + { + var authContext = this._authContextGetter.Get(); + var userId = authContext.UserId; + // 由 Context 取得 UserId + var member = Member.GetFakeMembers().FirstOrDefault(p => p.UserId == userId); + + this._logger.LogInformation(2000, "found {@Data}", member); + + return this.Ok(member); + } +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/JobBank1111.Job.WebAPI.csproj" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/JobBank1111.Job.WebAPI.csproj" new file mode 100644 index 00000000..541a8922 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/JobBank1111.Job.WebAPI.csproj" @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Models/Member.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Models/Member.cs" new file mode 100644 index 00000000..966a6b2e --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Models/Member.cs" @@ -0,0 +1,30 @@ +namespace JobBank1111.Job.WebAPI.Models; + +internal class Member +{ + public string UserId { get; set; } + + public int Age { get; set; } + + public string Name { get; set; } + + public static IEnumerable GetFakeMembers() + { + return new List() + { + new() + { + UserId = "yao", + Age = 19, + Name = "小章" + }, + new() + { + UserId = "yao1", + Age = 21, + Name = "小章1" + }, + + }; + } +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Program.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Program.cs" new file mode 100644 index 00000000..5d78d3bd --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Program.cs" @@ -0,0 +1,71 @@ +using JobBank1111.Infrastructure.TraceContext; +using JobBank1111.Job.WebAPI; +using Serilog; +using Serilog.Events; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/host-.txt", rollingInterval: RollingInterval.Hour) + .CreateLogger(); +Log.Information("Starting web host"); + +try +{ + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + builder.Services.AddControllers(); + + 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) + ); + + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + // builder.Services.AddScoped>(); + // builder.Services.AddScoped>(p => p.GetService>()); + // builder.Services.AddScoped>(p => p.GetService>()); + + builder.Services.AddSingleton>(); + builder.Services.AddSingleton>(p => p.GetService>()); + builder.Services.AddSingleton>(p => p.GetService>()); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseSerilogRequestLogging(); + app.UseHttpsRedirection(); + + app.UseAuthorization(); + app.UseMiddleware(); + app.MapControllers(); + app.Run(); + return 0; +} +catch (Exception ex) +{ + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} + +public partial class Program { } diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Properties/launchSettings.json" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Properties/launchSettings.json" new file mode 100644 index 00000000..6807ef37 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/Properties/launchSettings.json" @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:40069", + "sslPort": 44377 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5294", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7004;http://localhost:5294", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/SysHeaderNames.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/SysHeaderNames.cs" new file mode 100644 index 00000000..71fa54ae --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/SysHeaderNames.cs" @@ -0,0 +1,6 @@ +namespace JobBank1111.Job.WebAPI; + +public abstract class SysHeaderNames +{ + public const string TraceId = "x-trace-id"; +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/TraceContextMiddleware.cs" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/TraceContextMiddleware.cs" new file mode 100644 index 00000000..d8c26432 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/TraceContextMiddleware.cs" @@ -0,0 +1,78 @@ +using System.Security.Claims; +using JobBank1111.Infrastructure; +using JobBank1111.Infrastructure.TraceContext; +using JobBank1111.Job.WebAPI.Models; + +namespace JobBank1111.Job.WebAPI; + +public class TraceContextMiddleware +{ + private readonly RequestDelegate _next; + + public TraceContextMiddleware(RequestDelegate next) + { + this._next = next; + } + + public async Task Invoke(HttpContext httpContext, ILogger logger) + { + var traceId = httpContext.Request.Headers[SysHeaderNames.TraceId].FirstOrDefault(); + + //// 若調用端沒有傳入 traceId,則產生一個新的 traceId + if (string.IsNullOrWhiteSpace(traceId)) + { + traceId = httpContext.TraceIdentifier; + } + + // 模擬登入 + Signin(httpContext); + + if (httpContext.User.Identity.IsAuthenticated == false) + { + httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; + await httpContext.Response.WriteAsJsonAsync(new Failure + { + Code = FailureCode.Unauthorized, + Message = "not login", + }); + return; + } + + var userId = httpContext.User.Identity.Name; + + // 寫入 trace context 到 object context setter + var authContextSetter = httpContext.RequestServices.GetService>(); + authContextSetter.Set(new AuthContext + { + TraceId = traceId, + UserId = userId + }); + + // 附加 traceId 與 userId 到 log 中 + using var _ = logger.BeginScope("{Location},{TraceId},{UserId}", + "TW", traceId, userId); + + // 附加 traceId 到 response header 中 + IContextGetter? contextGetter = httpContext.RequestServices.GetService>(); + var traceContext = contextGetter.Get(); + httpContext.Response.Headers.TryAdd(SysHeaderNames.TraceId, traceContext.TraceId); + + await this._next.Invoke(httpContext); + } + + /// + /// 假的登入 + /// + /// + private static void Signin(HttpContext context) + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "yao"), + new Claim(ClaimTypes.Name, "yao"), + }; + var identity = new ClaimsIdentity(claims, "Bearer"); + var principal = new ClaimsPrincipal(identity); + context.User = principal; + } +} \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/appsettings.Development.json" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/appsettings.Development.json" new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/appsettings.Development.json" @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/appsettings.json" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/appsettings.json" new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/JobBank1111.Job.WebAPI/appsettings.json" @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/docker-compose.yml" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/docker-compose.yml" new file mode 100644 index 00000000..a9338570 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/docker-compose.yml" @@ -0,0 +1,7 @@ +services: + seq: + image: datalust/seq:latest + ports: + - "5341:80" + environment: + - ACCEPT_EULA=Y \ No newline at end of file diff --git "a/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/k8s/ns.yml" "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/k8s/ns.yml" new file mode 100644 index 00000000..78c095b8 --- /dev/null +++ "b/\345\260\210\346\241\210\347\257\204\346\234\254/Lab.Context.Trace/k8s/ns.yml" @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: Title +spec: + selector: + app: Title + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: NodePort \ No newline at end of file