├── .dockerignore
├── .github
├── FUNDING.yml
└── workflows
│ └── main.yml
├── .gitignore
├── Colors.API
├── Colors.API.csproj
├── Controllers
│ ├── AdminController.cs
│ ├── ColorsController.cs
│ ├── FilesController.cs
│ ├── StudentsController.cs
│ └── TodosController.cs
├── GlobalUsings.cs
├── Models
│ ├── ColorBrightnessDifference.cs
│ ├── ColorContrastRatio.cs
│ └── ColorDifference.cs
├── Program.cs
├── Services
│ └── ColorServices.cs
├── Startup.cs
├── appsettings.Development.json
└── appsettings.json
├── Colors.UnitTests
├── ColorServicesTests.cs
├── Colors.UnitTests.csproj
├── GlobalUsings.cs
└── ModelsTests.cs
├── Colors.sln
├── Colors.sln.DotSettings
├── Dockerfile
├── LICENSE
├── README.md
└── swagger-ui.png
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.classpath
2 | **/.dockerignore
3 | **/.env
4 | **/.git
5 | **/.gitignore
6 | **/.project
7 | **/.settings
8 | **/.toolstarget
9 | **/.vs
10 | **/.vscode
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/azds.yaml
15 | **/bin
16 | **/charts
17 | **/docker-compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | LICENSE
25 | README.md
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: changhuixu
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: HerokuContainer
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v1
14 | - name: Build and deploy the Docker image
15 | env:
16 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
17 | APP_NAME: ${{ 'icolors' }}
18 | run: |
19 | docker login --username=_ --password=$HEROKU_API_KEY registry.heroku.com
20 | heroku container:push web -a $APP_NAME
21 | heroku container:release web -a $APP_NAME
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015/2017 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # Visual Studio 2017 auto generated files
33 | Generated\ Files/
34 |
35 | # MSTest test Results
36 | [Tt]est[Rr]esult*/
37 | [Bb]uild[Ll]og.*
38 |
39 | # NUNIT
40 | *.VisualState.xml
41 | TestResult.xml
42 |
43 | # Build Results of an ATL Project
44 | [Dd]ebugPS/
45 | [Rr]eleasePS/
46 | dlldata.c
47 |
48 | # Benchmark Results
49 | BenchmarkDotNet.Artifacts/
50 |
51 | # .NET Core
52 | project.lock.json
53 | project.fragment.lock.json
54 | artifacts/
55 | **/Properties/launchSettings.json
56 |
57 | # StyleCop
58 | StyleCopReport.xml
59 |
60 | # Files built by Visual Studio
61 | *_i.c
62 | *_p.c
63 | *_i.h
64 | *.ilk
65 | *.meta
66 | *.obj
67 | *.iobj
68 | *.pch
69 | *.pdb
70 | *.ipdb
71 | *.pgc
72 | *.pgd
73 | *.rsp
74 | *.sbr
75 | *.tlb
76 | *.tli
77 | *.tlh
78 | *.tmp
79 | *.tmp_proj
80 | *.log
81 | *.vspscc
82 | *.vssscc
83 | .builds
84 | *.pidb
85 | *.svclog
86 | *.scc
87 |
88 | # Chutzpah Test files
89 | _Chutzpah*
90 |
91 | # Visual C++ cache files
92 | ipch/
93 | *.aps
94 | *.ncb
95 | *.opendb
96 | *.opensdf
97 | *.sdf
98 | *.cachefile
99 | *.VC.db
100 | *.VC.VC.opendb
101 |
102 | # Visual Studio profiler
103 | *.psess
104 | *.vsp
105 | *.vspx
106 | *.sap
107 |
108 | # Visual Studio Trace Files
109 | *.e2e
110 |
111 | # TFS 2012 Local Workspace
112 | $tf/
113 |
114 | # Guidance Automation Toolkit
115 | *.gpState
116 |
117 | # ReSharper is a .NET coding add-in
118 | _ReSharper*/
119 | *.[Rr]e[Ss]harper
120 | *.DotSettings.user
121 |
122 | # JustCode is a .NET coding add-in
123 | .JustCode
124 |
125 | # TeamCity is a build add-in
126 | _TeamCity*
127 |
128 | # DotCover is a Code Coverage Tool
129 | *.dotCover
130 |
131 | # AxoCover is a Code Coverage Tool
132 | .axoCover/*
133 | !.axoCover/settings.json
134 |
135 | # Visual Studio code coverage results
136 | *.coverage
137 | *.coveragexml
138 |
139 | # NCrunch
140 | _NCrunch_*
141 | .*crunch*.local.xml
142 | nCrunchTemp_*
143 |
144 | # MightyMoose
145 | *.mm.*
146 | AutoTest.Net/
147 |
148 | # Web workbench (sass)
149 | .sass-cache/
150 |
151 | # Installshield output folder
152 | [Ee]xpress/
153 |
154 | # DocProject is a documentation generator add-in
155 | DocProject/buildhelp/
156 | DocProject/Help/*.HxT
157 | DocProject/Help/*.HxC
158 | DocProject/Help/*.hhc
159 | DocProject/Help/*.hhk
160 | DocProject/Help/*.hhp
161 | DocProject/Help/Html2
162 | DocProject/Help/html
163 |
164 | # Click-Once directory
165 | publish/
166 |
167 | # Publish Web Output
168 | *.[Pp]ublish.xml
169 | *.azurePubxml
170 | # Note: Comment the next line if you want to checkin your web deploy settings,
171 | # but database connection strings (with potential passwords) will be unencrypted
172 | *.pubxml
173 | *.publishproj
174 |
175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
176 | # checkin your Azure Web App publish settings, but sensitive information contained
177 | # in these scripts will be unencrypted
178 | PublishScripts/
179 |
180 | # NuGet Packages
181 | *.nupkg
182 | # The packages folder can be ignored because of Package Restore
183 | **/[Pp]ackages/*
184 | # except build/, which is used as an MSBuild target.
185 | !**/[Pp]ackages/build/
186 | # Uncomment if necessary however generally it will be regenerated when needed
187 | #!**/[Pp]ackages/repositories.config
188 | # NuGet v3's project.json files produces more ignorable files
189 | *.nuget.props
190 | *.nuget.targets
191 |
192 | # Microsoft Azure Build Output
193 | csx/
194 | *.build.csdef
195 |
196 | # Microsoft Azure Emulator
197 | ecf/
198 | rcf/
199 |
200 | # Windows Store app package directories and files
201 | AppPackages/
202 | BundleArtifacts/
203 | Package.StoreAssociation.xml
204 | _pkginfo.txt
205 | *.appx
206 |
207 | # Visual Studio cache files
208 | # files ending in .cache can be ignored
209 | *.[Cc]ache
210 | # but keep track of directories ending in .cache
211 | !*.[Cc]ache/
212 |
213 | # Others
214 | ClientBin/
215 | ~$*
216 | *~
217 | *.dbmdl
218 | *.dbproj.schemaview
219 | *.jfm
220 | *.pfx
221 | *.publishsettings
222 | orleans.codegen.cs
223 |
224 | # Including strong name files can present a security risk
225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
226 | #*.snk
227 |
228 | # Since there are multiple workflows, uncomment next line to ignore bower_components
229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
230 | #bower_components/
231 |
232 | # RIA/Silverlight projects
233 | Generated_Code/
234 |
235 | # Backup & report files from converting an old project file
236 | # to a newer Visual Studio version. Backup files are not needed,
237 | # because we have git ;-)
238 | _UpgradeReport_Files/
239 | Backup*/
240 | UpgradeLog*.XML
241 | UpgradeLog*.htm
242 | ServiceFabricBackup/
243 | *.rptproj.bak
244 |
245 | # SQL Server files
246 | *.mdf
247 | *.ldf
248 | *.ndf
249 |
250 | # Business Intelligence projects
251 | *.rdl.data
252 | *.bim.layout
253 | *.bim_*.settings
254 | *.rptproj.rsuser
255 |
256 | # Microsoft Fakes
257 | FakesAssemblies/
258 |
259 | # GhostDoc plugin setting file
260 | *.GhostDoc.xml
261 |
262 | # Node.js Tools for Visual Studio
263 | .ntvs_analysis.dat
264 | node_modules/
265 |
266 | # Visual Studio 6 build log
267 | *.plg
268 |
269 | # Visual Studio 6 workspace options file
270 | *.opt
271 |
272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
273 | *.vbw
274 |
275 | # Visual Studio LightSwitch build output
276 | **/*.HTMLClient/GeneratedArtifacts
277 | **/*.DesktopClient/GeneratedArtifacts
278 | **/*.DesktopClient/ModelManifest.xml
279 | **/*.Server/GeneratedArtifacts
280 | **/*.Server/ModelManifest.xml
281 | _Pvt_Extensions
282 |
283 | # Paket dependency manager
284 | .paket/paket.exe
285 | paket-files/
286 |
287 | # FAKE - F# Make
288 | .fake/
289 |
290 | # JetBrains Rider
291 | .idea/
292 | *.sln.iml
293 |
294 | # CodeRush
295 | .cr/
296 |
297 | # Python Tools for Visual Studio (PTVS)
298 | __pycache__/
299 | *.pyc
300 |
301 | # Cake - Uncomment if you are using it
302 | # tools/**
303 | # !tools/packages.config
304 |
305 | # Tabs Studio
306 | *.tss
307 |
308 | # Telerik's JustMock configuration file
309 | *.jmconfig
310 |
311 | # BizTalk build output
312 | *.btp.cs
313 | *.btm.cs
314 | *.odx.cs
315 | *.xsd.cs
316 |
317 | # OpenCover UI analysis results
318 | OpenCover/
319 |
320 | # Azure Stream Analytics local run output
321 | ASALocalRun/
322 |
323 | # MSBuild Binary and Structured Log
324 | *.binlog
325 |
326 | # NVidia Nsight GPU debugger configuration file
327 | *.nvuser
328 |
329 | # MFractors (Xamarin productivity tool) working folder
330 | .mfractor/
331 |
332 | CSharpLabs/RunExeFromWebApi/WebApi/App_Data/
333 |
--------------------------------------------------------------------------------
/Colors.API/Colors.API.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | true
6 | $(NoWarn);1591
7 | enable
8 | enable
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Colors.API/Controllers/AdminController.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace Colors.API.Controllers;
4 |
5 | [ApiController]
6 | [ApiExplorerSettings(GroupName = "v3")]
7 | [Route("api/[controller]")]
8 | public class AdminController : ControllerBase
9 | {
10 | [HttpGet("sys-info")]
11 | public ActionResult GetSystemInformation()
12 | {
13 | return Ok(new { os = Environment.OSVersion.VersionString, net = RuntimeInformation.FrameworkDescription });
14 | }
15 | }
--------------------------------------------------------------------------------
/Colors.API/Controllers/ColorsController.cs:
--------------------------------------------------------------------------------
1 | namespace Colors.API.Controllers;
2 |
3 | ///
4 | /// A set of APIs to convert colors and compute color metrics
5 | ///
6 | [ApiController]
7 | [ApiExplorerSettings(GroupName = "v1")]
8 | [Produces("application/json")]
9 | [Route("api/[controller]")]
10 | public class ColorsController(ILogger logger) : ControllerBase
11 | {
12 | ///
13 | /// Get the color object from a HEX color format.
14 | ///
15 | ///
16 | /// Sample request:
17 | ///
18 | /// GET /api/colors/00FF00/argb
19 | ///
20 | /// Sample response:
21 | ///
22 | /// {
23 | /// "r": 0,
24 | /// "g": 255,
25 | /// "b": 0,
26 | /// "a": 255,
27 | /// "isKnownColor": false,
28 | /// "isEmpty": false,
29 | /// "isNamedColor": false,
30 | /// "isSystemColor": false,
31 | /// "name": "ff00ff00"
32 | /// }
33 | ///
34 | ///
35 | /// Color: a HEX number. Example: 00FF00
36 | /// Returns the color object.
37 | /// If any color string in the query parameters is invalid.
38 | [HttpGet("{color}/argb")]
39 | [ProducesResponseType(typeof(Color), StatusCodes.Status200OK)]
40 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
41 | public ActionResult GetArgb(string color)
42 | {
43 | if (!int.TryParse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
44 | {
45 | ModelState.AddModelError(nameof(color), $"The color [{color}] is invalid. This API endpoint accepts a HEX number as a color.");
46 | return BadRequest(ModelState);
47 | }
48 |
49 | return ColorTranslator.FromHtml("#" + color);
50 | }
51 |
52 |
53 | ///
54 | /// Get the luminance of a color.
55 | ///
56 | ///
57 | /// Sample request:
58 | ///
59 | /// GET /api/colors/00FF00/luminance
60 | ///
61 | /// Sample response:
62 | ///
63 | /// 0.7152
64 | ///
65 | ///
66 | /// Color: a HEX number
67 | /// Returns the luminance value of a color.
68 | /// If any color string in the query parameters is invalid.
69 | [HttpGet("{color}/luminance")]
70 | [ProducesResponseType(typeof(double), StatusCodes.Status200OK)]
71 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
72 | public ActionResult GetLuminance(string color)
73 | {
74 | if (!int.TryParse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
75 | {
76 | ModelState.AddModelError(nameof(color), $"The color [{color}] is invalid. This API endpoint accepts a HEX number as a color.");
77 | return BadRequest(ModelState);
78 | }
79 |
80 | return ColorServices.GetLuminance(ColorTranslator.FromHtml("#" + color));
81 | }
82 |
83 |
84 | ///
85 | /// Get the brightness of a color.
86 | ///
87 | ///
88 | /// Sample request:
89 | ///
90 | /// GET /api/colors/00FF00/brightness
91 | ///
92 | /// Sample response:
93 | ///
94 | /// 149.685
95 | ///
96 | ///
97 | /// Color: a HEX number
98 | /// Returns the brightness value of a color.
99 | /// If any color string in the query parameters is invalid.
100 | [HttpGet("{color}/brightness")]
101 | [ProducesResponseType(typeof(double), StatusCodes.Status200OK)]
102 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
103 | public ActionResult GetBrightness(string color)
104 | {
105 | if (!int.TryParse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
106 | {
107 | ModelState.AddModelError(nameof(color), $"The color [{color}] is invalid. This API endpoint accepts a HEX number as a color.");
108 | return BadRequest(ModelState);
109 | }
110 |
111 | return ColorServices.GetBrightness(ColorTranslator.FromHtml("#" + color));
112 | }
113 |
114 |
115 | ///
116 | /// Computes the contrast ratio between two colors.
117 | ///
118 | ///
119 | /// Sample request:
120 | ///
121 | /// GET /api/colors/contrast-ratio
122 | /// {
123 | /// "fc": "00FF00",
124 | /// "bc": "FFFFFF"
125 | /// }
126 | ///
127 | /// Sample response:
128 | ///
129 | /// {
130 | /// "color1": {
131 | /// "r": 0,
132 | /// "g": 255,
133 | /// "b": 0,
134 | /// "a": 255,
135 | /// "isKnownColor": false,
136 | /// "isEmpty": false,
137 | /// "isNamedColor": false,
138 | /// "isSystemColor": false,
139 | /// "name": "ff00ff00"
140 | /// },
141 | /// "color2": {
142 | /// "r": 255,
143 | /// "g": 255,
144 | /// "b": 255,
145 | /// "a": 255,
146 | /// "isKnownColor": false,
147 | /// "isEmpty": false,
148 | /// "isNamedColor": false,
149 | /// "isSystemColor": false,
150 | /// "name": "ffffffff"
151 | /// },
152 | /// "ratio": 1.372
153 | /// }
154 | ///
155 | ///
156 | /// Foreground Color: a HEX number
157 | /// Background Color: a HEX number
158 | ///
159 | /// Returns the ColorContrastRatio object
160 | ///
161 | /// The contrast ratio is calculated based on [this formula](https://www.w3.org/TR/AERT/#color-contrast)
162 | ///
163 | /// If any color string in the query parameters is invalid.
164 | [HttpGet("contrast-ratio")]
165 | [ProducesResponseType(typeof(ColorContrastRatio), StatusCodes.Status200OK)]
166 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
167 | public ActionResult GetContrastRatio(string fc, string bc)
168 | {
169 | if (string.IsNullOrWhiteSpace(fc))
170 | {
171 | ModelState.AddModelError(nameof(fc), "Foreground color is missing. This API endpoint accepts a HEX number as a color.");
172 | return BadRequest(ModelState);
173 | }
174 | if (string.IsNullOrWhiteSpace(bc))
175 | {
176 | ModelState.AddModelError(nameof(bc), "Background color is missing. This API endpoint accepts a HEX number as a color.");
177 | return BadRequest(ModelState);
178 | }
179 |
180 | if (!int.TryParse(fc, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
181 | {
182 | ModelState.AddModelError(nameof(fc), $"The color [{fc}] is invalid. This API endpoint accepts a HEX number as a color.");
183 | return BadRequest(ModelState);
184 | }
185 | if (!int.TryParse(bc, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
186 | {
187 | ModelState.AddModelError(nameof(bc), $"The color [{bc}] is invalid. This API endpoint accepts a HEX number as a color.");
188 | return BadRequest(ModelState);
189 | }
190 |
191 | logger.LogInformation("Checking the Contrast Ratio between Foreground Color [{fc}] and Background Color [{bc}].", fc, bc);
192 | var fColor = ColorTranslator.FromHtml("#" + fc);
193 | var bColor = ColorTranslator.FromHtml("#" + bc);
194 | return new ColorContrastRatio(fColor, bColor);
195 | }
196 |
197 |
198 | ///
199 | /// Computes the Brightness Difference between two colors.
200 | ///
201 | ///
202 | /// Sample request:
203 | ///
204 | /// GET /api/colors/brightness-difference
205 | /// {
206 | /// "fc": "00FF00",
207 | /// "bc": "FFFFFF"
208 | /// }
209 | ///
210 | /// Sample response:
211 | ///
212 | /// {
213 | /// "color1": {
214 | /// "r": 0,
215 | /// "g": 255,
216 | /// "b": 0,
217 | /// "a": 255,
218 | /// "isKnownColor": false,
219 | /// "isEmpty": false,
220 | /// "isNamedColor": false,
221 | /// "isSystemColor": false,
222 | /// "name": "ff00ff00"
223 | /// },
224 | /// "color2": {
225 | /// "r": 255,
226 | /// "g": 255,
227 | /// "b": 255,
228 | /// "a": 255,
229 | /// "isKnownColor": false,
230 | /// "isEmpty": false,
231 | /// "isNamedColor": false,
232 | /// "isSystemColor": false,
233 | /// "name": "ffffffff"
234 | /// },
235 | /// "diff": 105.315,
236 | /// "acceptable": false
237 | /// }
238 | ///
239 | ///
240 | /// Foreground Color: a HEX number
241 | /// Background Color: a HEX number
242 | /// Returns the ColorBrightnessDifference object
243 | /// If any color string in the query parameters is invalid.
244 | [HttpGet("brightness-difference")]
245 | [ProducesResponseType(typeof(ColorBrightnessDifference), StatusCodes.Status200OK)]
246 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
247 | public ActionResult GetBrightnessDifference(string fc, string bc)
248 | {
249 | if (string.IsNullOrWhiteSpace(fc))
250 | {
251 | ModelState.AddModelError(nameof(fc), "Foreground color is missing. This API endpoint accepts a HEX number as a color.");
252 | return BadRequest(ModelState);
253 | }
254 | if (string.IsNullOrWhiteSpace(bc))
255 | {
256 | ModelState.AddModelError(nameof(bc), "Background color is missing. This API endpoint accepts a HEX number as a color.");
257 | return BadRequest(ModelState);
258 | }
259 |
260 | if (!int.TryParse(fc, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
261 | {
262 | ModelState.AddModelError(nameof(fc), $"The color [{fc}] is invalid. This API endpoint accepts a HEX number as a color.");
263 | return BadRequest(ModelState);
264 | }
265 | if (!int.TryParse(bc, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
266 | {
267 | ModelState.AddModelError(nameof(bc), $"The color [{bc}] is invalid. This API endpoint accepts a HEX number as a color.");
268 | return BadRequest(ModelState);
269 | }
270 |
271 | logger.LogInformation("Checking the ColorBrightnessDifference between Foreground Color [{fc}] and Background Color [{bc}].", fc, bc);
272 | var fColor = ColorTranslator.FromHtml("#" + fc);
273 | var bColor = ColorTranslator.FromHtml("#" + bc);
274 | return new ColorBrightnessDifference(fColor, bColor);
275 | }
276 |
277 |
278 | ///
279 | /// Computes the Color Difference between two colors.
280 | ///
281 | ///
282 | /// Sample request:
283 | ///
284 | /// GET /api/colors/color-difference
285 | /// {
286 | /// "fc": "00FF00",
287 | /// "bc": "FFFFFF"
288 | /// }
289 | ///
290 | /// Sample response:
291 | ///
292 | /// {
293 | /// "color1": {
294 | /// "r": 0,
295 | /// "g": 255,
296 | /// "b": 0,
297 | /// "a": 255,
298 | /// "isKnownColor": false,
299 | /// "isEmpty": false,
300 | /// "isNamedColor": false,
301 | /// "isSystemColor": false,
302 | /// "name": "ff00ff00"
303 | /// },
304 | /// "color2": {
305 | /// "r": 255,
306 | /// "g": 255,
307 | /// "b": 255,
308 | /// "a": 255,
309 | /// "isKnownColor": false,
310 | /// "isEmpty": false,
311 | /// "isNamedColor": false,
312 | /// "isSystemColor": false,
313 | /// "name": "ffffffff"
314 | /// },
315 | /// "diff": 510,
316 | /// "acceptable": true
317 | /// }
318 | ///
319 | ///
320 | /// Foreground Color: a HEX number
321 | /// Background Color: a HEX number
322 | /// Returns the ColorBrightnessDifference object
323 | /// If any color string in the query parameters is invalid.
324 | [HttpGet("color-difference")]
325 | [ProducesResponseType(typeof(ColorDifference), StatusCodes.Status200OK)]
326 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
327 | public ActionResult GetColorDifference(string fc, string bc)
328 | {
329 | if (string.IsNullOrWhiteSpace(fc))
330 | {
331 | ModelState.AddModelError(nameof(fc), "Foreground color is missing. This API endpoint accepts a HEX number as a color.");
332 | return BadRequest(ModelState);
333 | }
334 | if (string.IsNullOrWhiteSpace(bc))
335 | {
336 | ModelState.AddModelError(nameof(bc), "Background color is missing. This API endpoint accepts a HEX number as a color.");
337 | return BadRequest(ModelState);
338 | }
339 |
340 | if (!int.TryParse(fc, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
341 | {
342 | ModelState.AddModelError(nameof(fc), $"The color [{fc}] is invalid. This API endpoint accepts a HEX number as a color.");
343 | return BadRequest(ModelState);
344 | }
345 | if (!int.TryParse(bc, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
346 | {
347 | ModelState.AddModelError(nameof(bc), $"The color [{bc}] is invalid. This API endpoint accepts a HEX number as a color.");
348 | return BadRequest(ModelState);
349 | }
350 |
351 | logger.LogInformation("Checking the ColorDifference between Foreground Color [{fc}] and Background Color [{bc}].", fc, bc);
352 | var fColor = ColorTranslator.FromHtml("#" + fc);
353 | var bColor = ColorTranslator.FromHtml("#" + bc);
354 | return new ColorDifference(fColor, bColor);
355 | }
356 | }
--------------------------------------------------------------------------------
/Colors.API/Controllers/FilesController.cs:
--------------------------------------------------------------------------------
1 | namespace Colors.API.Controllers;
2 |
3 | [ApiController]
4 | [ApiExplorerSettings(GroupName = "v2")]
5 | [Produces("application/json")]
6 | [Route("api/[controller]")]
7 | public class FilesController(ILogger logger) : ControllerBase
8 | {
9 | ///
10 | /// Download a file. This demo will generate a txt file.
11 | ///
12 | ///
13 | ///
14 | [HttpGet("{id:int}", Name = "Download a File by FileID")]
15 | public IActionResult Download(int id)
16 | {
17 | return File("hello world"u8.ToArray(), "text/plain", $"test-{id}.txt");
18 | }
19 |
20 | ///
21 | /// Upload a file. This demo is dummy and only waits 2 seconds.
22 | ///
23 | ///
24 | ///
25 | [HttpPost("single-file")]
26 | public async Task Upload(IFormFile file)
27 | {
28 | logger.LogInformation("validating the file {fileName}", file.FileName);
29 | logger.LogInformation("saving file");
30 | await Task.Delay(2000); // validate file type/format/size, scan virus, save it to a storage
31 | logger.LogInformation("file saved.");
32 | }
33 |
34 | ///
35 | /// Upload two files. This demo is dummy and only waits 2 seconds.
36 | ///
37 | ///
38 | ///
39 | ///
40 | [HttpPost("two-files")]
41 | public async Task Upload(IFormFile file1, IFormFile file2)
42 | {
43 | logger.LogInformation("validating the file {fileName}", file1.FileName);
44 | logger.LogInformation("validating the file {fileName}", file2.FileName);
45 | logger.LogInformation("saving files");
46 | await Task.Delay(2000);
47 | logger.LogInformation("files saved.");
48 | }
49 |
50 | ///
51 | /// Upload multiple files. This demo is dummy and only waits 2 seconds.
52 | ///
53 | ///
54 | ///
55 | [HttpPost("multiple-files")]
56 | public async Task Upload(List files)
57 | {
58 | logger.LogInformation("validating {n} files", files.Count);
59 | foreach (var file in files)
60 | {
61 | logger.LogInformation("saving file {fileName}", file.FileName);
62 | await Task.Delay(1000);
63 | }
64 | logger.LogInformation("All files saved.");
65 | }
66 |
67 | [HttpDelete("{id:int}")]
68 | [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
69 | public async Task Delete(int id)
70 | {
71 | logger.LogInformation("deleting file ID=[{id}]", id);
72 | await Task.Delay(1500);
73 | return true;
74 | }
75 | }
--------------------------------------------------------------------------------
/Colors.API/Controllers/StudentsController.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace Colors.API.Controllers;
5 |
6 | ///
7 | /// An example controller for testing multipart/form-data
submission
8 | ///
9 | [ApiController]
10 | [Produces("application/json")]
11 | [Route("api/[controller]")]
12 | public class StudentsController(ILogger logger) : ControllerBase
13 | {
14 |
15 | ///
16 | /// View a form
17 | ///
18 | /// Student ID
19 | /// Form ID
20 | ///
21 | [HttpGet("{id:int}/forms/{formId:int}")]
22 | [ProducesResponseType(typeof(FormSubmissionResult), StatusCodes.Status200OK)]
23 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
24 | public async Task ViewForm(int id, int formId)
25 | {
26 | logger.LogInformation("viewing the form#{formId} for Student ID={id}", formId, id);
27 | await Task.Delay(1000);
28 | return new FormSubmissionResult { FormId = formId, StudentId = id };
29 | }
30 |
31 | ///
32 | /// Submit a form which contains a key-value pair and a file.
33 | ///
34 | /// Student ID
35 | /// A form which contains the FormId and a file
36 | ///
37 | [HttpPost("{id:int}/forms")]
38 | [ProducesResponseType(typeof(FormSubmissionResult), StatusCodes.Status201Created)]
39 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
40 | public async Task> SubmitForm(int id, [FromForm] StudentForm form)
41 | {
42 | logger.LogInformation("validating the form#{formId} for Student ID={id}", form.FormId ,id);
43 | logger.LogInformation("saving file [{fileName}]", form.StudentFile.FileName);
44 | await Task.Delay(1500);
45 | logger.LogInformation("file saved.");
46 | var result = new FormSubmissionResult { FormId = form.FormId, StudentId = id };
47 | return CreatedAtAction(nameof(ViewForm), new { id, form.FormId }, result);
48 | }
49 | [HttpPost("{id:int}/forms2")]
50 | [ProducesResponseType(typeof(FormSubmissionResult), StatusCodes.Status201Created)]
51 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
52 | public async Task> SubmitForm2(int id, [FromBody] StudentForm2 form)
53 | {
54 | logger.LogInformation("validating the form#{formId} for Student ID={id}", form.FormId, id);
55 | await Task.Delay(1500);
56 | logger.LogInformation("file saved.");
57 | var result = new FormSubmissionResult { FormId = form.FormId, StudentId = id };
58 | return CreatedAtAction(nameof(ViewForm), new { id, form.FormId }, result);
59 | }
60 | [ApiExplorerSettings(IgnoreApi = true)]
61 | [HttpDelete("{id:int}/forms/{formId:int}")]
62 | [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
63 | public async Task Delete(int id, int formId)
64 | {
65 | logger.LogInformation("This API is hidden in Swagger UI and Swagger JSON doc.");
66 | logger.LogInformation("deleting the form#{formId} for student ID=[{id}]", formId, id);
67 | await Task.Delay(1500);
68 | return true;
69 | }
70 |
71 | ///
72 | /// Get students by residential type
73 | ///
74 | /// Residential Type. **Default**: InState
.
75 | ///
76 | [HttpGet("")]
77 | public ActionResult GetStudentsByResidentialType(ResidentialType residentialType = ResidentialType.InState)
78 | {
79 | logger.LogInformation("query {residentialType} students", residentialType);
80 | if (residentialType == ResidentialType.International)
81 | {
82 | logger.LogInformation("found 10000 students.");
83 | }
84 | return Ok();
85 | }
86 | }
87 |
88 | public class StudentForm
89 | {
90 | [Required] public int FormId { get; set; }
91 | [Required] public IFormFile StudentFile { get; set; } = null!;
92 | [Required] public DateTime SignatureDateTime { get; set; }
93 | }
94 |
95 | public class StudentForm2
96 | {
97 | [Required] public int FormId { get; set; }
98 | [Required] public DateTime SignatureDateTime { get; set; }
99 | }
100 | public class FormSubmissionResult
101 | {
102 | public int StudentId { get; set; }
103 | public int FormId { get; set; }
104 | }
105 |
106 | [JsonConverter(typeof(JsonStringEnumConverter))]
107 | public enum ResidentialType
108 | {
109 | InState,
110 | OutOfState,
111 | International
112 | }
--------------------------------------------------------------------------------
/Colors.API/Controllers/TodosController.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace Colors.API.Controllers;
4 |
5 | ///
6 | /// An example controller performs CRUD operations on TodoItems
7 | ///
8 | [ApiController]
9 | [Produces("application/json", "text/plain")]
10 | [Route("api/[controller]")]
11 | public class TodosController : ControllerBase
12 | {
13 | private static readonly List TodoItems =
14 | [
15 | new TodoItem
16 | {
17 | Id = 1,
18 | Name = "feed fish",
19 | IsComplete = true
20 | },
21 |
22 | new TodoItem
23 | {
24 | Id = 2,
25 | Name = "walk dog",
26 | IsComplete = true
27 | }
28 | ];
29 |
30 | ///
31 | /// Gets a TodoItem by ID
32 | ///
33 | ///
34 | ///
35 | /// Sample request:
36 | ///
37 | /// GET /api/todos/1
38 | ///
39 | /// Sample response:
40 | ///
41 | /// {
42 | /// "id": 1,
43 | /// "name": "feed fish",
44 | /// "isComplete": true
45 | /// }
46 | ///
47 | ///
48 | /// The ID of a TodoItem.
49 | ///
50 | /// Returns the item with the specified ID
51 | /// If the item is not found.
52 | [HttpGet("{id:int}")]
53 | [ProducesResponseType(StatusCodes.Status200OK)]
54 | [ProducesResponseType(StatusCodes.Status404NotFound)]
55 | public ActionResult GetTodoItem(int id)
56 | {
57 | var todoItem = TodoItems.FirstOrDefault(x => x.Id == id);
58 |
59 | if (todoItem == null)
60 | {
61 | return NotFound();
62 | }
63 |
64 | return todoItem;
65 | }
66 |
67 | ///
68 | /// Creates a TodoItem.
69 | ///
70 | ///
71 | /// Sample request:
72 | ///
73 | /// POST /api/todos
74 | /// {
75 | /// "id": 3,
76 | /// "name": "push up",
77 | /// "isComplete": false
78 | /// }
79 | ///
80 | /// Sample response body:
81 | ///
82 | /// {
83 | /// "id": 3,
84 | /// "name": "push up",
85 | /// "isComplete": false
86 | /// }
87 | ///
88 | /// Sample response header:
89 | ///
90 | /// content-type: application/json; charset=utf-8
91 | /// date: Wed, 01 Apr 2020 20:30:05 GMT
92 | /// location: http://localhost:5000/api/Todos/3
93 | /// server: Kestrel
94 | /// transfer-encoding: chunked
95 | ///
96 | ///
97 | ///
98 | /// A newly created TodoItem
99 | /// Returns the newly created item
100 | /// If the item is null
101 | [HttpPost]
102 | [ProducesResponseType(StatusCodes.Status201Created)]
103 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
104 | public ActionResult Create(TodoItem item)
105 | {
106 | TodoItems.Add(item);
107 | return CreatedAtAction(nameof(GetTodoItem), new { id = item.Id }, item);
108 | }
109 |
110 | ///
111 | /// Modifies a ToDoItem
112 | ///
113 | ///
114 | ///
115 | ///
116 | [HttpPut("{id:int}")]
117 | public IActionResult PutTodoItem(int id, TodoItem todoItem)
118 | {
119 | if (id != todoItem.Id)
120 | {
121 | return BadRequest();
122 | }
123 |
124 | var todo = TodoItems.FirstOrDefault(x => x.Id == id);
125 | if (todo == null)
126 | {
127 | return NotFound();
128 | }
129 |
130 | todo.Name = todoItem.Name;
131 | todo.IsComplete = todoItem.IsComplete;
132 |
133 | return NoContent();
134 | }
135 |
136 | ///
137 | /// Deletes a TodoItem
138 | ///
139 | ///
140 | ///
141 | /// # Header 1
142 | ///
143 | /// ## Header 2
144 | ///
145 | /// 1. Item 1
146 | /// 1. Item 2
147 | ///
148 | /// * Item 1
149 | /// * Item 2
150 | ///
151 | /// inline `code`
152 | ///
153 | /// [link](#)
154 | ///
155 | /// ### Table
156 | ///
157 | /// Column 1 | Column 2 | Column 2
158 | /// -------- | -------- | ---------
159 | /// Value 1 | Value 2 | Value 3
160 | ///
161 | ///
162 | ///
163 | ///
164 | ///
165 | [HttpDelete("{id:int}")]
166 | public ActionResult DeleteTodoItem(int id)
167 | {
168 | var todoItem = TodoItems.FirstOrDefault(x => x.Id == id);
169 | if (todoItem == null)
170 | {
171 | return NotFound();
172 | }
173 |
174 | TodoItems.Remove(todoItem);
175 | return todoItem;
176 | }
177 | }
178 |
179 | ///
180 | /// A TodoItem tracks a task
181 | ///
182 | public class TodoItem
183 | {
184 | ///
185 | /// The ID of the TodoItem
186 | ///
187 | /// 1
188 | public int Id { get; set; }
189 |
190 | ///
191 | /// The task name.
192 | ///
193 | ///
194 | /// feed fish
195 | ///
196 | [Required]
197 | public string Name { get; set; } = string.Empty;
198 |
199 | ///
200 | /// The task is completed or not. Default: false
.
201 | ///
202 | /// false
203 | public bool IsComplete { get; set; }
204 | }
--------------------------------------------------------------------------------
/Colors.API/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using System.Drawing;
2 | global using System.Globalization;
3 | global using Colors.API.Models;
4 | global using Colors.API.Services;
5 | global using Microsoft.AspNetCore.Mvc;
--------------------------------------------------------------------------------
/Colors.API/Models/ColorBrightnessDifference.cs:
--------------------------------------------------------------------------------
1 | namespace Colors.API.Models;
2 |
3 | public class ColorBrightnessDifference
4 | {
5 | public Color Color1 { get; }
6 | public Color Color2 { get; }
7 | public double Diff { get; }
8 | public bool Acceptable { get; }
9 | public const double Threshold = 125.0;
10 |
11 | public ColorBrightnessDifference(Color color1, Color color2)
12 | {
13 | Color1 = color1;
14 | Color2 = color2;
15 | Diff = Math.Round(Math.Abs(ColorServices.GetBrightness(color1) - ColorServices.GetBrightness(color2)), 3);
16 | Acceptable = Diff >= Threshold;
17 | }
18 | }
--------------------------------------------------------------------------------
/Colors.API/Models/ColorContrastRatio.cs:
--------------------------------------------------------------------------------
1 | namespace Colors.API.Models;
2 |
3 | public class ColorContrastRatio(Color color1, Color color2)
4 | {
5 | public Color Color1 { get; } = color1;
6 | public Color Color2 { get; } = color2;
7 | public double Ratio { get; } = ColorServices.GetContrastRatio(color1, color2);
8 | }
--------------------------------------------------------------------------------
/Colors.API/Models/ColorDifference.cs:
--------------------------------------------------------------------------------
1 | namespace Colors.API.Models;
2 |
3 | public class ColorDifference
4 | {
5 | public Color Color1 { get; }
6 | public Color Color2 { get; }
7 | public int Diff { get; }
8 | public bool Acceptable { get; }
9 | public const int Threshold = 500;
10 |
11 | public ColorDifference(Color color1, Color color2)
12 | {
13 | Color1 = color1;
14 | Color2 = color2;
15 | Diff = (Math.Max(color1.R, color2.R) - Math.Min(color1.R, color2.R)) +
16 | (Math.Max(color1.G, color2.G) - Math.Min(color1.G, color2.G)) +
17 | (Math.Max(color1.B, color2.B) - Math.Min(color1.B, color2.B));
18 | Acceptable = Diff >= Threshold;
19 | }
20 | }
--------------------------------------------------------------------------------
/Colors.API/Program.cs:
--------------------------------------------------------------------------------
1 | namespace Colors.API;
2 |
3 | public class Program
4 | {
5 | public static void Main(string[] args)
6 | {
7 | CreateHostBuilder(args).Build().Run();
8 | }
9 |
10 | public static IHostBuilder CreateHostBuilder(string[] args) =>
11 | Host.CreateDefaultBuilder(args)
12 | .ConfigureWebHostDefaults(webBuilder =>
13 | {
14 | webBuilder.UseStartup();
15 | });
16 | }
--------------------------------------------------------------------------------
/Colors.API/Services/ColorServices.cs:
--------------------------------------------------------------------------------
1 | namespace Colors.API.Services;
2 |
3 | public class ColorServices
4 | {
5 | // https://www.w3.org/TR/AERT/#color-contrast
6 | /*
7 | * Two colors provide good color visibility if the brightness difference and the color difference between the two colors are greater than a set range.
8 |
9 | Color brightness is determined by the following formula:
10 | ((Red value X 299) + (Green value X 587) + (Blue value X 114)) / 1000
11 | Note: This algorithm is taken from a formula for converting RGB values to YIQ values. This brightness value gives a perceived brightness for a color.
12 |
13 | Color difference is determined by the following formula:
14 | (maximum (Red value 1, Red value 2) - minimum (Red value 1, Red value 2)) + (maximum (Green value 1, Green value 2) - minimum (Green value 1, Green value 2)) + (maximum (Blue value 1, Blue value 2) - minimum (Blue value 1, Blue value 2))
15 |
16 | The rage for color brightness difference is 125. The range for color difference is 500.
17 | */
18 | public static double GetBrightness(Color color)
19 | {
20 | return (color.R * 299 + color.G * 587 + color.B * 114) / 1000.0;
21 | }
22 |
23 | public static double GetLuminance(Color color)
24 | {
25 | return 0.2126 * GetsRgb(color.R) + 0.7152 * GetsRgb(color.G) + 0.0722 * GetsRgb(color.B);
26 |
27 | static double GetsRgb(byte c)
28 | {
29 | var s = c / 255.0;
30 | if (s <= 0.03928)
31 | {
32 | return s / 12.92;
33 | }
34 | return Math.Pow((s + 0.055) / 1.055, 2.4);
35 | }
36 | }
37 |
38 | // https://www.w3.org/TR/WCAG20/#relativeLuminanceDef
39 | public static double GetContrastRatio(Color foreground, Color background)
40 | {
41 | double ratio;
42 | var l1 = GetLuminance(foreground);
43 | var l2 = GetLuminance(background);
44 |
45 | if (l1 >= l2)
46 | {
47 | ratio = (l1 + 0.05) / (l2 + 0.05);
48 | }
49 | else
50 | {
51 | ratio = (l2 + 0.05) / (l1 + 0.05);
52 | }
53 | return Math.Floor(ratio * 1000) / 1000; // round to 3 decimal places
54 | }
55 | }
--------------------------------------------------------------------------------
/Colors.API/Startup.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using Microsoft.AspNetCore.HttpOverrides;
3 | using Microsoft.OpenApi.Models;
4 |
5 | namespace Colors.API;
6 |
7 | public class Startup
8 | {
9 | // This method gets called by the runtime. Use this method to add services to the container.
10 | public void ConfigureServices(IServiceCollection services)
11 | {
12 | services.AddControllers();
13 | services.AddSwaggerGen(c =>
14 | {
15 | c.SwaggerDoc("v1", new OpenApiInfo
16 | {
17 | Version = "v1",
18 | Title = "ToDo API",
19 | Description = "A simple example ASP.NET Core Web API",
20 | TermsOfService = new Uri("https://example.com/terms"),
21 | Contact = new OpenApiContact
22 | {
23 | Name = "GitHub Repository",
24 | Email = string.Empty,
25 | Url = new Uri("https://github.com/dotnet-labs/HerokuContainer")
26 | }
27 | });
28 | c.SwaggerDoc("v2", new OpenApiInfo { Version = "v1.2", Title = "File Upload API" });
29 | c.SwaggerDoc("v3", new OpenApiInfo { Version = "v3", Title = "Misc API" });
30 |
31 | var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
32 | var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
33 | c.IncludeXmlComments(xmlPath, true);
34 | });
35 |
36 | services.AddHttpsRedirection(options => { options.HttpsPort = 443; });
37 | services.Configure(options =>
38 | {
39 | options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
40 | ForwardedHeaders.XForwardedProto;
41 | options.KnownNetworks.Clear();
42 | options.KnownProxies.Clear();
43 | });
44 | }
45 |
46 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
47 | public void Configure(IApplicationBuilder app, IHostEnvironment env)
48 | {
49 | if (env.IsDevelopment())
50 | {
51 | app.UseDeveloperExceptionPage();
52 | }
53 |
54 | app.UseHsts();
55 | app.UseForwardedHeaders();
56 | if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DYNO")))
57 | {
58 | app.UseHttpsRedirection();
59 | }
60 |
61 | app.UseSwagger();
62 | app.UseSwaggerUI(c =>
63 | {
64 | c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
65 | c.SwaggerEndpoint("/swagger/v2/swagger.json", "v2");
66 | c.SwaggerEndpoint("/swagger/v3/swagger.json", "v3");
67 | c.DocumentTitle = "Todo APIs";
68 | c.DefaultModelsExpandDepth(0);
69 | c.RoutePrefix = string.Empty;
70 | });
71 |
72 | app.UseRouting();
73 | app.UseAuthorization();
74 |
75 | app.UseEndpoints(endpoints =>
76 | {
77 | endpoints.MapControllers();
78 | });
79 | }
80 | }
--------------------------------------------------------------------------------
/Colors.API/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Colors.API/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/Colors.UnitTests/ColorServicesTests.cs:
--------------------------------------------------------------------------------
1 | namespace Colors.UnitTests;
2 |
3 | [TestClass]
4 | public class ColorServicesTests
5 | {
6 | [TestMethod]
7 | public void TestMethod1()
8 | {
9 | var color = Color.FromArgb(100, 12, 134);
10 | Console.WriteLine(color.A); //255
11 | Console.WriteLine(color.R); //100
12 | Console.WriteLine(color.G); //12
13 | Console.WriteLine(color.B); //134
14 | Console.WriteLine(color); //Color[A = 255, R = 100, G = 12, B = 134]
15 | Console.WriteLine(color.GetBrightness()); //0.28627452
16 | Console.WriteLine(color.GetHashCode()); //1754876516
17 | Console.WriteLine(color.GetHue()); //283.2787
18 | Console.WriteLine(color.GetSaturation()); //0.8356164
19 |
20 | color = ColorTranslator.FromHtml("#00FF00");
21 | Console.WriteLine(color.A); //255
22 | Console.WriteLine(color.R); //0
23 | Console.WriteLine(color.G); //255
24 | Console.WriteLine(color.B); //0
25 | Console.WriteLine(color); //Color [A=255, R=0, G=255, B=0]
26 | Console.WriteLine(color.GetBrightness()); //0.5
27 | Console.WriteLine(color.GetHashCode()); //-1019428136
28 | Console.WriteLine(color.GetHue()); //120
29 | Console.WriteLine(color.GetSaturation()); //1
30 |
31 | Console.WriteLine(ColorTranslator.ToHtml(color)); //#00FF00
32 | }
33 |
34 | [TestMethod]
35 | public void GetLuminanceTests()
36 | {
37 | // the relative brightness of any point in a color space, normalized to 0 for darkest black and 1 for lightest white
38 | Assert.AreEqual(0, ColorServices.GetLuminance(Color.Black));
39 | Assert.AreEqual(1, ColorServices.GetLuminance(Color.White));
40 | Assert.AreEqual(0.2126, ColorServices.GetLuminance(Color.Red));
41 | Assert.AreEqual(0.48170267036309633, ColorServices.GetLuminance(Color.Orange));
42 | Assert.AreEqual(0.9278, ColorServices.GetLuminance(Color.Yellow));
43 | Assert.AreEqual(0.7152, ColorServices.GetLuminance(Color.FromArgb(255, 0, 255, 0)));
44 | Assert.AreEqual(0.0722, ColorServices.GetLuminance(Color.Blue));
45 | Assert.AreEqual(0.031075614863369856, ColorServices.GetLuminance(Color.Indigo));
46 | Assert.AreEqual(0.40315452986676326, ColorServices.GetLuminance(Color.Violet));
47 | }
48 |
49 | [TestMethod]
50 | public void GetColorContrastTests()
51 | {
52 | Assert.AreEqual(1.0, ColorServices.GetContrastRatio(Color.White, Color.White));
53 | Assert.AreEqual(21.0, ColorServices.GetContrastRatio(Color.Black, Color.White));
54 | // Pure red(#FF0000) has a ratio of 4:1. I am red text.
55 | // Pure green (#00FF00) has a very low ratio of 1.4:1. I am green text.
56 | // Pure blue (#0000FF) has a contrast ratio of 8.6:1.I am blue text.
57 | Assert.AreEqual(3.998, ColorServices.GetContrastRatio(Color.Red, Color.White));
58 | Assert.AreEqual(1.372, ColorServices.GetContrastRatio(Color.FromArgb(255, 0, 255, 0), Color.White));
59 | Assert.AreEqual(8.592, ColorServices.GetContrastRatio(Color.Blue, Color.White));
60 |
61 | Assert.AreEqual(1.0, ColorServices.GetContrastRatio(ColorTranslator.FromHtml("#a"), ColorTranslator.FromHtml("#b")));
62 | }
63 |
64 | [DataTestMethod]
65 | [DataRow("#FFFFFF", 255)]
66 | [DataRow("#33FF33", 170.748)]
67 | [DataRow("#333333", 51)]
68 | [DataRow("#000000", 0)]
69 | public void GetColorBrightnessTests(string hex, double brightness)
70 | {
71 | var color = ColorTranslator.FromHtml(hex);
72 | Assert.AreEqual(brightness, ColorServices.GetBrightness(color));
73 | }
74 | }
--------------------------------------------------------------------------------
/Colors.UnitTests/Colors.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | false
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | all
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Colors.UnitTests/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using System.Drawing;
2 | global using System.Text.Json;
3 | global using Colors.API.Models;
4 | global using Colors.API.Services;
5 | global using Microsoft.VisualStudio.TestTools.UnitTesting;
--------------------------------------------------------------------------------
/Colors.UnitTests/ModelsTests.cs:
--------------------------------------------------------------------------------
1 | namespace Colors.UnitTests;
2 |
3 | [TestClass]
4 | public class ModelsTests
5 | {
6 | [DataTestMethod]
7 | [DataRow("#FFFFFF", "#000000", 255, true)]
8 | [DataRow("#000000", "#FFFFFF", 255, true)]
9 | [DataRow("#33FF33", "#333333", 119.748, false)]
10 | [DataRow("#333333", "#33FF33", 119.748, false)]
11 | public void ShouldCorrectlyComputeColorBrightnessDifference(string hex1, string hex2, double diff, bool acceptable)
12 | {
13 | var color1 = ColorTranslator.FromHtml(hex1);
14 | var color2 = ColorTranslator.FromHtml(hex2);
15 | var brightnessDiff = new ColorBrightnessDifference(color1, color2);
16 | Assert.AreEqual(diff, brightnessDiff.Diff);
17 | Assert.AreEqual(acceptable, brightnessDiff.Acceptable);
18 | }
19 |
20 |
21 | [DataTestMethod]
22 | [DataRow("#FFFFFF", "#000000", 765, true)]
23 | [DataRow("#000000", "#FFFFFF", 765, true)]
24 | [DataRow("#33FF33", "#333333", 204, false)]
25 | [DataRow("#333333", "#33FF33", 204, false)]
26 | public void ShouldCorrectlyComputeColorDifference(string hex1, string hex2, double diff, bool acceptable)
27 | {
28 | var color1 = ColorTranslator.FromHtml(hex1);
29 | var color2 = ColorTranslator.FromHtml(hex2);
30 | var colorDifference = new ColorDifference(color1, color2);
31 | Assert.AreEqual(diff, colorDifference.Diff);
32 | Assert.AreEqual(acceptable, colorDifference.Acceptable);
33 | }
34 |
35 | [TestMethod]
36 | public void T()
37 | {
38 | const string json = "{\"Utc\": \"2020-10-05T05:29:00Z\",\"Local\": \"2020-10-05T07:29:00+00:00\" }";
39 |
40 | var foo = JsonSerializer.Deserialize(json)!;
41 |
42 | Assert.AreEqual(new DateTime(2020, 10, 5, 5, 29, 0), foo.Utc);
43 | Assert.AreEqual(new DateTime(2020, 10, 5, 2, 29, 0), foo.Local);
44 | Console.WriteLine($"UTC1:{foo.Utc} ({foo.Utc.Kind}).\r\nLOC:{foo.Local} ({foo.Local.Kind})");
45 | Console.WriteLine($"UTC->Local:{foo.Utc.ToLocalTime()} ({foo.Utc.Kind}).\r\nLOC:{foo.Local} ({foo.Local.Kind})");
46 | }
47 | public sealed class CustomTime
48 | {
49 | public DateTime Utc { get; set; }
50 | public DateTime Local { get; set; }
51 | }
52 | }
--------------------------------------------------------------------------------
/Colors.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.2.32630.192
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Colors.API", "Colors.API\Colors.API.csproj", "{85547D88-F189-4C1B-89AC-31BBCF786F57}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Colors.UnitTests", "Colors.UnitTests\Colors.UnitTests.csproj", "{F407FE28-48B4-413D-B965-8C146BF92D02}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {85547D88-F189-4C1B-89AC-31BBCF786F57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {85547D88-F189-4C1B-89AC-31BBCF786F57}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {85547D88-F189-4C1B-89AC-31BBCF786F57}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {85547D88-F189-4C1B-89AC-31BBCF786F57}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {F407FE28-48B4-413D-B965-8C146BF92D02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {F407FE28-48B4-413D-B965-8C146BF92D02}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {F407FE28-48B4-413D-B965-8C146BF92D02}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {F407FE28-48B4-413D-B965-8C146BF92D02}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {C144CA55-14DF-424B-9E4A-9307E496EF3C}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/Colors.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
3 | True
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # NuGet restore
2 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
3 | WORKDIR /src
4 | COPY *.sln .
5 | COPY Colors.UnitTests/*.csproj Colors.UnitTests/
6 | COPY Colors.API/*.csproj Colors.API/
7 | RUN dotnet restore
8 | COPY . .
9 |
10 | # testing
11 | FROM build AS testing
12 | WORKDIR /src/Colors.API
13 | RUN dotnet build
14 | WORKDIR /src/Colors.UnitTests
15 | RUN dotnet test
16 |
17 | # publish
18 | FROM build AS publish
19 | WORKDIR /src/Colors.API
20 | RUN dotnet publish -c Release -o /src/publish
21 |
22 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
23 | WORKDIR /app
24 | COPY --from=publish /src/publish .
25 | ENTRYPOINT ["dotnet", "Colors.API.dll"]
26 | # heroku uses the following
27 | # CMD ASPNETCORE_URLS=http://*:$PORT dotnet Colors.API.dll
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Changhui Xu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dockerized ASP.NET Core Web API app in Heroku
2 |
3 |
4 |
5 | ## Updates
6 |
7 | The solution is updated to ASP.NET 8
8 |
9 | ## [Medium Article 1: Deploy a Dockerized ASP.NET Core Web API app to Heroku](https://codeburst.io/deploy-a-containerized-asp-net-core-app-to-heroku-using-github-actions-9e54c72db943)
10 |
11 | In this blog post, we will create a containerized ASP.NET Core 3.1 Web API project, and set up a CI/CD pipeline using GitHub Actions. In the GitHub workflow, we will build and test the Web API project, and deploy the final docker image to Heroku.
12 |
13 | ## [Medium Article 2: Get Started with Swashbuckle and ASP.NET Core](https://codeburst.io/get-started-with-swashbuckle-and-asp-net-core-fd3a75350aac)
14 |
15 | This article is intended to add some supplementary information to the official documentation in Microsoft Docs. My goal is to connect the dots between the code/comments and the Swagger UI elements.
16 |
17 | ## [Medium Article 3: File Upload via Swagger](https://codeburst.io/file-uploading-in-swagger-e6c21b54d036)
18 |
19 | In this article, we will go over examples about uploading a single file, uploading a list of files, and uploading a file in a FormData object.
20 |
21 | ---
22 |
23 | ## [API Website](https://icolors.herokuapp.com)
24 |
25 |
26 |
27 | ## License
28 |
29 | Feel free to use the code in this repository as it is under MIT license.
30 |
31 |
32 |
--------------------------------------------------------------------------------
/swagger-ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dotnet-labs/HerokuContainer/d6006e19a8e80ec30519f38230d616893d9af7e0/swagger-ui.png
--------------------------------------------------------------------------------