├── .gitignore
├── BookCovers.API
├── BookCovers.API.csproj
├── Controllers
│ └── BookCoversController.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── appsettings.Development.json
└── appsettings.json
├── Books.Api
├── ArrayModelBinder.cs
├── Books.Api.csproj
├── BooksProfile.cs
├── Contexts
│ └── BooksContext.cs
├── Controllers
│ └── BooksController.cs
├── Entities
│ ├── Author.cs
│ └── Book.cs
├── ExternalModels
│ └── BookCover.cs
├── Models
│ ├── Book.cs
│ ├── BookCover.cs
│ ├── BookForCreation.cs
│ └── BookWithCovers.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── Services
│ ├── BooksRepository.cs
│ └── IBooksRepository.cs
├── appsettings.Development.json
└── appsettings.json
├── Books.Legacy
├── Books.Legacy.csproj
└── ComplicatedPageCalculator.cs
├── Books.sln
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/aspnetcore
3 |
4 | ### ASPNETCore ###
5 | ## Ignore Visual Studio temporary files, build results, and
6 | ## files generated by popular Visual Studio add-ons.
7 |
8 | # User-specific files
9 | *.suo
10 | *.user
11 | *.userosscache
12 | *.sln.docstates
13 |
14 | # User-specific files (MonoDevelop/Xamarin Studio)
15 | *.userprefs
16 |
17 | # Build results
18 | [Dd]ebug/
19 | [Dd]ebugPublic/
20 | [Rr]elease/
21 | [Rr]eleases/
22 | x64/
23 | x86/
24 | bld/
25 | [Bb]in/
26 | [Oo]bj/
27 | [Ll]og/
28 |
29 | # Visual Studio 2015 cache/options directory
30 | .vs/
31 | # Uncomment if you have tasks that create the project's static files in wwwroot
32 | #wwwroot/
33 |
34 | # MSTest test Results
35 | [Tt]est[Rr]esult*/
36 | [Bb]uild[Ll]og.*
37 |
38 | # NUNIT
39 | *.VisualState.xml
40 | TestResult.xml
41 |
42 | # Build Results of an ATL Project
43 | [Dd]ebugPS/
44 | [Rr]eleasePS/
45 | dlldata.c
46 |
47 | # DNX
48 | project.lock.json
49 | project.fragment.lock.json
50 | artifacts/
51 |
52 | *_i.c
53 | *_p.c
54 | *_i.h
55 | *.ilk
56 | *.meta
57 | *.obj
58 | *.pch
59 | *.pdb
60 | *.pgc
61 | *.pgd
62 | *.rsp
63 | *.sbr
64 | *.tlb
65 | *.tli
66 | *.tlh
67 | *.tmp
68 | *.tmp_proj
69 | *.log
70 | *.vspscc
71 | *.vssscc
72 | .builds
73 | *.pidb
74 | *.svclog
75 | *.scc
76 |
77 | # Chutzpah Test files
78 | _Chutzpah*
79 |
80 | # Visual C++ cache files
81 | ipch/
82 | *.aps
83 | *.ncb
84 | *.opendb
85 | *.opensdf
86 | *.sdf
87 | *.cachefile
88 | *.VC.db
89 | *.VC.VC.opendb
90 |
91 | # Visual Studio profiler
92 | *.psess
93 | *.vsp
94 | *.vspx
95 | *.sap
96 |
97 | # TFS 2012 Local Workspace
98 | $tf/
99 |
100 | # Guidance Automation Toolkit
101 | *.gpState
102 |
103 | # ReSharper is a .NET coding add-in
104 | _ReSharper*/
105 | *.[Rr]e[Ss]harper
106 | *.DotSettings.user
107 |
108 | # JustCode is a .NET coding add-in
109 | .JustCode
110 |
111 | # TeamCity is a build add-in
112 | _TeamCity*
113 |
114 | # DotCover is a Code Coverage Tool
115 | *.dotCover
116 |
117 | # Visual Studio code coverage results
118 | *.coverage
119 | *.coveragexml
120 |
121 | # NCrunch
122 | _NCrunch_*
123 | .*crunch*.local.xml
124 | nCrunchTemp_*
125 |
126 | # MightyMoose
127 | *.mm.*
128 | AutoTest.Net/
129 |
130 | # Web workbench (sass)
131 | .sass-cache/
132 |
133 | # Installshield output folder
134 | [Ee]xpress/
135 |
136 | # DocProject is a documentation generator add-in
137 | DocProject/buildhelp/
138 | DocProject/Help/*.HxT
139 | DocProject/Help/*.HxC
140 | DocProject/Help/*.hhc
141 | DocProject/Help/*.hhk
142 | DocProject/Help/*.hhp
143 | DocProject/Help/Html2
144 | DocProject/Help/html
145 |
146 | # Click-Once directory
147 | publish/
148 |
149 | # Publish Web Output
150 | *.[Pp]ublish.xml
151 | *.azurePubxml
152 | # TODO: Comment the next line if you want to checkin your web deploy settings
153 | # but database connection strings (with potential passwords) will be unencrypted
154 | *.pubxml
155 | *.publishproj
156 |
157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
158 | # checkin your Azure Web App publish settings, but sensitive information contained
159 | # in these scripts will be unencrypted
160 | PublishScripts/
161 |
162 | # NuGet Packages
163 | *.nupkg
164 | # The packages folder can be ignored because of Package Restore
165 | **/packages/*
166 | # except build/, which is used as an MSBuild target.
167 | !**/packages/build/
168 | # Uncomment if necessary however generally it will be regenerated when needed
169 | #!**/packages/repositories.config
170 | # NuGet v3's project.json files produces more ignoreable files
171 | *.nuget.props
172 | *.nuget.targets
173 |
174 | # Microsoft Azure Build Output
175 | csx/
176 | *.build.csdef
177 |
178 | # Microsoft Azure Emulator
179 | ecf/
180 | rcf/
181 |
182 | # Windows Store app package directories and files
183 | AppPackages/
184 | BundleArtifacts/
185 | Package.StoreAssociation.xml
186 | _pkginfo.txt
187 |
188 | # Visual Studio cache files
189 | # files ending in .cache can be ignored
190 | *.[Cc]ache
191 | # but keep track of directories ending in .cache
192 | !*.[Cc]ache/
193 |
194 | # Others
195 | ClientBin/
196 | ~$*
197 | *~
198 | *.dbmdl
199 | *.dbproj.schemaview
200 | *.jfm
201 | *.pfx
202 | *.publishsettings
203 | node_modules/
204 | orleans.codegen.cs
205 |
206 | # Since there are multiple workflows, uncomment next line to ignore bower_components
207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
208 | #bower_components/
209 |
210 | # RIA/Silverlight projects
211 | Generated_Code/
212 |
213 | # Backup & report files from converting an old project file
214 | # to a newer Visual Studio version. Backup files are not needed,
215 | # because we have git ;-)
216 | _UpgradeReport_Files/
217 | Backup*/
218 | UpgradeLog*.XML
219 | UpgradeLog*.htm
220 |
221 | # SQL Server files
222 | *.mdf
223 | *.ldf
224 |
225 | # Business Intelligence projects
226 | *.rdl.data
227 | *.bim.layout
228 | *.bim_*.settings
229 |
230 | # Microsoft Fakes
231 | FakesAssemblies/
232 |
233 | # GhostDoc plugin setting file
234 | *.GhostDoc.xml
235 |
236 | # Node.js Tools for Visual Studio
237 | .ntvs_analysis.dat
238 |
239 | # Visual Studio 6 build log
240 | *.plg
241 |
242 | # Visual Studio 6 workspace options file
243 | *.opt
244 |
245 | # Visual Studio LightSwitch build output
246 | **/*.HTMLClient/GeneratedArtifacts
247 | **/*.DesktopClient/GeneratedArtifacts
248 | **/*.DesktopClient/ModelManifest.xml
249 | **/*.Server/GeneratedArtifacts
250 | **/*.Server/ModelManifest.xml
251 | _Pvt_Extensions
252 |
253 | # Paket dependency manager
254 | .paket/paket.exe
255 | paket-files/
256 |
257 | # FAKE - F# Make
258 | .fake/
259 |
260 | # JetBrains Rider
261 | .idea/
262 | *.sln.iml
263 |
264 | # CodeRush
265 | .cr/
266 |
267 | # Python Tools for Visual Studio (PTVS)
268 | __pycache__/
269 | *.pyc
270 |
271 | # Cake - Uncomment if you are using it
272 | # tools/
273 |
274 |
275 | # End of https://www.gitignore.io/api/aspnetcore
--------------------------------------------------------------------------------
/BookCovers.API/BookCovers.API.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/BookCovers.API/Controllers/BookCoversController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace BookCovers.API.Controllers
4 | {
5 | [Route("api/bookcovers")]
6 | [ApiController]
7 | public class BookCoversController : ControllerBase
8 | {
9 | [HttpGet("{name}")]
10 | public async Task GetBookCover(string name, bool returnFault = false)
11 | {
12 | // if returnFault is true, wait 100ms and
13 | // return an Internal Server Error
14 | if (returnFault)
15 | {
16 | await Task.Delay(100);
17 | return new StatusCodeResult(500);
18 | }
19 |
20 | // generate a "book cover" (byte array) between 2 and 10MB
21 | var random = new Random();
22 | int fakeCoverBytes = random.Next(2097152, 10485760);
23 | byte[] fakeCover = new byte[fakeCoverBytes];
24 | random.NextBytes(fakeCover);
25 |
26 | return Ok(new
27 | {
28 | Name = name,
29 | Content = fakeCover
30 | });
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/BookCovers.API/Program.cs:
--------------------------------------------------------------------------------
1 | var builder = WebApplication.CreateBuilder(args);
2 |
3 | // Add services to the container.
4 |
5 | builder.Services.AddControllers();
6 |
7 | var app = builder.Build();
8 |
9 | // Configure the HTTP request pipeline.
10 |
11 | app.UseAuthorization();
12 |
13 | app.MapControllers();
14 |
15 | app.Run();
16 |
--------------------------------------------------------------------------------
/BookCovers.API/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:52644",
8 | "sslPort": 44339
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "api/values",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "BookCovers.API": {
21 | "commandName": "Project",
22 | "launchBrowser": false,
23 | "launchUrl": "api/values",
24 | "applicationUrl": "http://localhost:52644",
25 | "environmentVariables": {
26 | "ASPNETCORE_ENVIRONMENT": "Development"
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/BookCovers.API/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/BookCovers.API/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning"
5 | }
6 | },
7 | "AllowedHosts": "*"
8 | }
9 |
--------------------------------------------------------------------------------
/Books.Api/ArrayModelBinder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.ModelBinding;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.ComponentModel;
5 | using System.Linq;
6 | using System.Reflection;
7 | using System.Threading.Tasks;
8 |
9 | namespace Books.Api
10 | {
11 | public class ArrayModelBinder : IModelBinder
12 | {
13 | public Task BindModelAsync(ModelBindingContext bindingContext)
14 | {
15 | // Our binder works only on enumerable types
16 | if (!bindingContext.ModelMetadata.IsEnumerableType)
17 | {
18 | bindingContext.Result = ModelBindingResult.Failed();
19 | return Task.CompletedTask;
20 | }
21 |
22 | // Get the inputted value through the value provider
23 | var value = bindingContext.ValueProvider
24 | .GetValue(bindingContext.ModelName).ToString();
25 |
26 | // If that value is null or whitespace, we return null
27 | if (string.IsNullOrWhiteSpace(value))
28 | {
29 | bindingContext.Result = ModelBindingResult.Success(null);
30 | return Task.CompletedTask;
31 | }
32 |
33 | // The value isn't null or whitespace,
34 | // and the type of the model is enumerable.
35 | // Get the enumerable's type, and a converter
36 | var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
37 | var converter = TypeDescriptor.GetConverter(elementType);
38 |
39 | // Convert each item in the value list to the enumerable type
40 | var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
41 | .Select(x => converter.ConvertFromString(x.Trim()))
42 | .ToArray();
43 |
44 | // Create an array of that type, and set it as the Model value
45 | var typedValues = Array.CreateInstance(elementType, values.Length);
46 | values.CopyTo(typedValues, 0);
47 | bindingContext.Model = typedValues;
48 |
49 | // return a successful result, passing in the Model
50 | bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
51 | return Task.CompletedTask;
52 | }
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/Books.Api/Books.Api.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Books.Api/BooksProfile.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace Books.Api
8 | {
9 | public class BooksProfile : Profile
10 | {
11 | public BooksProfile()
12 | {
13 | CreateMap()
14 | .ForMember(dest => dest.Author, opt => opt.MapFrom(src =>
15 | $"{src.Author.FirstName} {src.Author.LastName}"));
16 |
17 | CreateMap();
18 |
19 | CreateMap()
20 | .ForMember(dest => dest.Author, opt => opt.MapFrom(src =>
21 | $"{src.Author.FirstName} {src.Author.LastName}"));
22 |
23 | CreateMap, Models.BookWithCovers>()
24 | .ForMember(dest => dest.BookCovers, opt => opt.MapFrom(src =>
25 | src));
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Books.Api/Contexts/BooksContext.cs:
--------------------------------------------------------------------------------
1 | using Books.Api.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace Books.Api.Contexts
5 | {
6 | public class BooksContext : DbContext
7 | {
8 | public DbSet Books { get; set; } = null!;
9 |
10 | public BooksContext(DbContextOptions options)
11 | : base(options)
12 | {
13 | }
14 |
15 | protected override void OnModelCreating(ModelBuilder modelBuilder)
16 | {
17 | // seed the database with dummy data
18 | modelBuilder.Entity().HasData(
19 | new Author()
20 | {
21 | Id = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"),
22 | FirstName = "George",
23 | LastName = "RR Martin"
24 | },
25 | new Author()
26 | {
27 | Id = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"),
28 | FirstName = "Stephen",
29 | LastName = "Fry"
30 | },
31 | new Author()
32 | {
33 | Id = Guid.Parse("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"),
34 | FirstName = "James",
35 | LastName = "Elroy"
36 | },
37 | new Author()
38 | {
39 | Id = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"),
40 | FirstName = "Douglas",
41 | LastName = "Adams"
42 | }
43 | );
44 |
45 | // seed the database with dummy data
46 | modelBuilder.Entity().HasData(
47 | new Book
48 | {
49 | Id = Guid.Parse("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"),
50 | AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"),
51 | Title = "The Winds of Winter",
52 | Description = "The book that seems impossible to write."
53 | },
54 | new Book
55 | {
56 | Id = Guid.Parse("d8663e5e-7494-4f81-8739-6e0de1bea7ee"),
57 | AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"),
58 | Title = "A Game of Thrones",
59 | Description = "A Game of Thrones is the first novel in A Song of Ice and Fire, a series of fantasy novels by American author George R. R. ... In the novel, recounting events from various points of view, Martin introduces the plot-lines of the noble houses of Westeros, the Wall, and the Targaryens."
60 | },
61 | new Book
62 | {
63 | Id = Guid.Parse("d173e20d-159e-4127-9ce9-b0ac2564ad97"),
64 | AuthorId = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"),
65 | Title = "Mythos",
66 | Description = "The Greek myths are amongst the best stories ever told, passed down through millennia and inspiring writers and artists as varied as Shakespeare, Michelangelo, James Joyce and Walt Disney. They are embedded deeply in the traditions, tales and cultural DNA of the West.You'll fall in love with Zeus, marvel at the birth of Athena, wince at Cronus and Gaia's revenge on Ouranos, weep with King Midas and hunt with the beautiful and ferocious Artemis. Spellbinding, informative and moving, Stephen Fry's Mythos perfectly captures these stories for the modern age - in all their rich and deeply human relevance."
67 | },
68 | new Book
69 | {
70 | Id = Guid.Parse("493c3228-3444-4a49-9cc0-e8532edc59b2"),
71 | AuthorId = Guid.Parse("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"),
72 | Title = "American Tabloid",
73 | Description = "American Tabloid is a 1995 novel by James Ellroy that chronicles the events surrounding three rogue American law enforcement officers from November 22, 1958 through November 22, 1963. Each becomes entangled in a web of interconnecting associations between the FBI, the CIA, and the mafia, which eventually leads to their collective involvement in the John F. Kennedy assassination."
74 | },
75 | new Book
76 | {
77 | Id = Guid.Parse("40ff5488-fdab-45b5-bc3a-14302d59869a"),
78 | AuthorId = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"),
79 | Title = "The Hitchhiker's Guide to the Galaxy",
80 | Description = "In The Hitchhiker's Guide to the Galaxy, the characters visit the legendary planet Magrathea, home to the now-collapsed planet-building industry, and meet Slartibartfast, a planetary coastline designer who was responsible for the fjords of Norway. Through archival recordings, he relates the story of a race of hyper-intelligent pan-dimensional beings who built a computer named Deep Thought to calculate the Answer to the Ultimate Question of Life, the Universe, and Everything."
81 | }
82 | );
83 |
84 | base.OnModelCreating(modelBuilder);
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/Books.Api/Controllers/BooksController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using Books.Api.Models;
3 | using Books.Api.Services;
4 | using Books.Legacy;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace Books.Api.Controllers
8 | {
9 | [Route("api/books")]
10 | [ApiController]
11 | public class BooksController : ControllerBase
12 | {
13 | private readonly IBooksRepository _booksRepository;
14 | private readonly IMapper _mapper;
15 | private readonly ILogger _logger;
16 | private readonly ComplicatedPageCalculator _complicatedPageCalculator;
17 |
18 | public BooksController(IBooksRepository booksRepository,
19 | IMapper mapper, ILogger logger,
20 | ComplicatedPageCalculator complicatedPageCalculator)
21 | {
22 | _booksRepository = booksRepository ??
23 | throw new ArgumentNullException(nameof(booksRepository));
24 | _mapper = mapper ??
25 | throw new ArgumentNullException(nameof(mapper));
26 | _logger = logger ??
27 | throw new ArgumentNullException(nameof(logger));
28 | _complicatedPageCalculator = complicatedPageCalculator ??
29 | throw new ArgumentNullException(nameof(complicatedPageCalculator));
30 | }
31 |
32 | [HttpGet("{id}", Name = "GetBook")]
33 | public async Task> GetBookWithBookCovers(Guid id)
34 | {
35 | var bookEntity = await _booksRepository.GetBookAsync(id);
36 |
37 | if (bookEntity == null)
38 | {
39 | return NotFound();
40 | }
41 |
42 | // get bookcovers
43 | var bookCovers = await _booksRepository.GetBookCoversAsync(id);
44 |
45 | // map book & covers into one BookWithCovers
46 | var mappedBook = _mapper.Map(bookEntity);
47 | return Ok(_mapper.Map(bookCovers, mappedBook));
48 | }
49 |
50 |
51 | [HttpPost]
52 | public async Task> CreateBook([FromBody] BookForCreation book)
53 | {
54 | var bookEntity = _mapper.Map(book);
55 | _booksRepository.AddBook(bookEntity);
56 |
57 | await _booksRepository.SaveChangesAsync();
58 |
59 | // Fetch (refetch) the book from the data store, including the author
60 | await _booksRepository.GetBookAsync(bookEntity.Id);
61 |
62 | return CreatedAtRoute("GetBook",
63 | new {id = bookEntity.Id},
64 | _mapper.Map(bookEntity));
65 | }
66 |
67 | #region IAsyncEnumerable sample right up to the database
68 |
69 | [HttpGet]
70 | public async IAsyncEnumerable YieldBooksOneByOneFromDatabase()
71 | {
72 | await foreach (var bookFromRepository in _booksRepository
73 | .GetBooksAsAsyncEnumerable())
74 | {
75 | // add a delay to visually see the effect
76 | await Task.Delay(500);
77 | yield return _mapper.Map(bookFromRepository);
78 | }
79 | }
80 |
81 | #endregion
82 |
83 | #region IAsyncEnumerable sample
84 | //[HttpGet]
85 | //public IActionResult YieldBooksOneByOne()
86 | //{
87 | // return Ok(GenerateBookStream());
88 | //}
89 |
90 | static async IAsyncEnumerable GenerateBookStream()
91 | {
92 | for (int i = 1; i <= 100; i++)
93 | {
94 | // add a delay to visually see the effect
95 | await Task.Delay(100);
96 | yield return new Book()
97 | {
98 | Author = "A yielded author",
99 | Description = "Nothing to see here",
100 | Title = $"Book {i}",
101 | Id = Guid.NewGuid()
102 | };
103 | }
104 | }
105 | #endregion
106 |
107 | #region Additional sample: GetBook with GetBookPages (code smell sample)
108 |
109 | // sample of bad code - don't await CPU-bound calls in ASP.NET Core,
110 | // call them in sync instead
111 | //[HttpGet("{id}", Name = "GetBook")]
112 | //public async Task GetBook(Guid id)
113 | //{
114 | // var bookEntity = await _booksRepository.GetBookAsync(id);
115 | //
116 | // if (bookEntity == null)
117 | // {
118 | // return NotFound();
119 | // }
120 | //
121 | // _logger.LogInformation($"ThreadId when entering GetBook: " +
122 | // $"{System.Threading.Thread.CurrentThread.ManagedThreadId}");
123 |
124 | // var pages = await GetBookPages(id);
125 |
126 | // return Ok(_mapper.Map(bookEntity));
127 | //}
128 |
129 | // private async Task GetBookPages(Guid id)
130 | // {
131 | // return await Task.Run(() =>
132 | // {
133 | // _logger.LogInformation($"ThreadId when calculating the amount of pages: " +
134 | // $"{System.Threading.Thread.CurrentThread.ManagedThreadId}");
135 | //
136 | // return _complicatedPageCalculator.CalculateBookPages(id);
137 | // });
138 | // }
139 |
140 | #endregion
141 |
142 | #region Additional sample: call legacy CPU-bound code (GetBook with direct call into CalculateBookPages)
143 |
144 | // [HttpGet("{id}", Name = "GetBook")]
145 | // public async Task GetBook(Guid id)
146 | // {
147 | // var bookEntity = await _booksRepository.GetBookAsync(id);
148 | //
149 | // if (bookEntity == null)
150 | // {
151 | // return NotFound();
152 | // }
153 | //
154 | // var pages = _complicatedPageCalculator.CalculateBookPages(id);
155 | //
156 | // return Ok(_mapper.Map(bookEntity));
157 | // }
158 |
159 | #endregion
160 |
161 | }
162 | }
--------------------------------------------------------------------------------
/Books.Api/Entities/Author.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel.DataAnnotations;
4 | using System.ComponentModel.DataAnnotations.Schema;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 |
8 | namespace Books.Api.Entities
9 | {
10 | [Table("Authors")]
11 | public class Author
12 | {
13 | [Key]
14 | public Guid Id { get; set; }
15 |
16 | [Required]
17 | [MaxLength(150)]
18 | public string FirstName { get; set; } = string.Empty;
19 |
20 | [Required]
21 | [MaxLength(150)]
22 | public string LastName { get; set; } = string.Empty;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Books.Api/Entities/Book.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.ComponentModel.DataAnnotations.Schema;
3 |
4 | namespace Books.Api.Entities
5 | {
6 | [Table("Books")]
7 | public class Book
8 | {
9 | [Key]
10 | public Guid Id { get; set; }
11 |
12 | [Required]
13 | [MaxLength(150)]
14 | public string Title { get; set; } = string.Empty;
15 |
16 | [MaxLength(2500)]
17 | public string Description { get; set; } = string.Empty;
18 |
19 | public Guid AuthorId { get; set; }
20 |
21 | // as per https://docs.microsoft.com/en-us/ef/core/miscellaneous/nullable-reference-types
22 | public Author Author { get; set; } = null!;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Books.Api/ExternalModels/BookCover.cs:
--------------------------------------------------------------------------------
1 | namespace Books.Api.ExternalModels
2 | {
3 | public class BookCover
4 | {
5 | public string Name { get; set; } = string.Empty;
6 | public byte[] Content { get; set; } = null!;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Books.Api/Models/Book.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Books.Api.Models
4 | {
5 | public class Book
6 | {
7 | public Guid Id { get; set; }
8 |
9 | public string Author { get; set; } = string.Empty;
10 |
11 | public string Title { get; set; } = string.Empty;
12 |
13 | public string Description { get; set; } = string.Empty;
14 |
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Books.Api/Models/BookCover.cs:
--------------------------------------------------------------------------------
1 | namespace Books.Api.Models
2 | {
3 | public class BookCover
4 | {
5 | public string Name { get; set; } = string.Empty;
6 |
7 | // don't return the bytes for the demo to avoid
8 | // long waiting times
9 | // public byte[] Content { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Books.Api/Models/BookForCreation.cs:
--------------------------------------------------------------------------------
1 | namespace Books.Api.Models
2 | {
3 | public class BookForCreation
4 | {
5 | public Guid AuthorId { get; set; }
6 |
7 | public string Title { get; set; } = string.Empty;
8 |
9 | public string Description { get; set; } = string.Empty;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Books.Api/Models/BookWithCovers.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Books.Api.Models
4 | {
5 | public class BookWithCovers : Book
6 | {
7 | public IEnumerable BookCovers { get; set; } = new List();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Books.Api/Program.cs:
--------------------------------------------------------------------------------
1 | using Books.Api.Contexts;
2 | using Books.Api.Services;
3 | using Books.Legacy;
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | var builder = WebApplication.CreateBuilder(args);
7 |
8 | // Add services to the container.
9 |
10 | builder.Services.AddControllers();
11 |
12 | builder.Services.AddDbContext(
13 | dbContextOptions => dbContextOptions.UseSqlite(
14 | builder.Configuration["ConnectionStrings:BooksDBConnectionString"]));
15 |
16 | builder.Services.AddScoped();
17 |
18 | builder.Services.AddTransient();
19 |
20 | builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
21 |
22 | builder.Services.AddHttpClient();
23 |
24 | var app = builder.Build();
25 |
26 | // Configure the HTTP request pipeline.
27 |
28 | app.UseAuthorization();
29 |
30 | app.MapControllers();
31 |
32 | app.Run();
33 |
--------------------------------------------------------------------------------
/Books.Api/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:59408",
7 | "sslPort": 44313
8 | }
9 | },
10 | "$schema": "http://json.schemastore.org/launchsettings.json",
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "api/books",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "Books.Api": {
21 | "commandName": "Project",
22 | "launchBrowser": true,
23 | "launchUrl": "api",
24 | "environmentVariables": {
25 | "ASPNETCORE_ENVIRONMENT": "Development"
26 | },
27 | "applicationUrl": "http://localhost:59408"
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/Books.Api/Services/BooksRepository.cs:
--------------------------------------------------------------------------------
1 | using Books.Api.Contexts;
2 | using Books.Api.Entities;
3 | using Books.Api.ExternalModels;
4 | using Microsoft.EntityFrameworkCore;
5 | using System.Text.Json;
6 |
7 | namespace Books.Api.Services
8 | {
9 | public class BooksRepository : IBooksRepository
10 | {
11 | private readonly BooksContext _context;
12 | private readonly IHttpClientFactory _httpClientFactory;
13 | private readonly ILogger _logger;
14 | private CancellationTokenSource _cancellationTokenSource = null!;
15 |
16 | public BooksRepository(BooksContext context,
17 | IHttpClientFactory httpClientFactory,
18 | ILogger logger)
19 | {
20 | _context = context ?? throw new ArgumentNullException(nameof(context));
21 | _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
22 | _logger = logger ?? throw new ArgumentNullException(nameof(logger));
23 | }
24 |
25 | public async Task GetBookAsync(Guid id)
26 | {
27 | if (id == Guid.Empty)
28 | {
29 | throw new ArgumentNullException(nameof(id));
30 | }
31 |
32 | return await _context.Books.Include(b => b.Author)
33 | .FirstOrDefaultAsync(b => b.Id == id);
34 | }
35 |
36 | public void AddBook(Book bookToAdd)
37 | {
38 | if (bookToAdd == null)
39 | {
40 | throw new ArgumentNullException(nameof(bookToAdd));
41 | }
42 |
43 | _context.Add(bookToAdd);
44 | }
45 |
46 | public async Task SaveChangesAsync()
47 | {
48 | return (await _context.SaveChangesAsync() > 0);
49 | }
50 |
51 | public async Task> GetBookCoversAsync(Guid bookId)
52 | {
53 | var httpClient = _httpClientFactory.CreateClient();
54 | var bookCovers = new List();
55 | _cancellationTokenSource = new CancellationTokenSource();
56 |
57 | // create a list of fake bookcovers
58 | var bookCoverUrls = new[]
59 | {
60 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover1",
61 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover2?returnFault=true",
62 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover3",
63 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover4",
64 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover5"
65 | };
66 |
67 | // foreach + await will run them in order. We prefer parallel.
68 |
69 | // create the tasks
70 | var downloadBookCoverTasksQuery =
71 | from bookCoverUrl
72 | in bookCoverUrls
73 | select DownloadBookCoverAsync(
74 | httpClient,
75 | bookCoverUrl,
76 | _cancellationTokenSource.Token);
77 |
78 | // start the tasks
79 | var downloadBookCoverTasks = downloadBookCoverTasksQuery.ToList();
80 |
81 | try
82 | {
83 | // get the results
84 | var results = await Task.WhenAll(downloadBookCoverTasks);
85 | // return results that aren't null
86 | return results.Where(r => r != null);
87 | }
88 | catch (OperationCanceledException operationCanceledException)
89 | {
90 | _logger.LogInformation($"{operationCanceledException.Message}");
91 | foreach (var task in downloadBookCoverTasks)
92 | {
93 | _logger.LogInformation($"Task {task.Id} has status {task.Status}");
94 | }
95 |
96 | return new List();
97 | }
98 | catch (Exception exception)
99 | {
100 | _logger.LogError($"{exception.Message}");
101 | throw;
102 | }
103 | }
104 |
105 | public async Task> GetBookCoversWithoutCancellationWithForEachAsync(Guid bookId)
106 | {
107 | var httpClient = _httpClientFactory.CreateClient();
108 | var bookCovers = new List();
109 |
110 | // create a list of fake bookcovers
111 | var bookCoverUrls = new[]
112 | {
113 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover1",
114 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover2?returnFault=true",
115 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover3",
116 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover4",
117 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover5"
118 | };
119 |
120 | // foreach + await will run them in order.
121 | foreach (var bookCoverUrl in bookCoverUrls)
122 | {
123 | var cover = await DownloadBookCoverWithoutCancellationSupportAsync(
124 | httpClient, bookCoverUrl);
125 |
126 | if (cover != null)
127 | {
128 | bookCovers.Add(cover);
129 | }
130 | }
131 |
132 | return bookCovers;
133 | }
134 |
135 | public async Task> GetBookCoversWithoutCancellationAsync(Guid bookId)
136 | {
137 | var httpClient = _httpClientFactory.CreateClient();
138 | var bookCovers = new List();
139 |
140 | // create a list of fake bookcovers
141 | var bookCoverUrls = new[]
142 | {
143 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover1",
144 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover2?returnFault=true",
145 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover3",
146 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover4",
147 | $"http://localhost:52644/api/bookcovers/{bookId}-dummycover5"
148 | };
149 |
150 | // foreach + await will run them in order. We prefer parallel.
151 | // create the tasks
152 | var downloadBookCoverTasksQuery =
153 | from bookCoverUrl
154 | in bookCoverUrls
155 | select DownloadBookCoverWithoutCancellationSupportAsync(
156 | httpClient, bookCoverUrl);
157 |
158 | // start the tasks
159 | var downloadBookCoverTasks = downloadBookCoverTasksQuery.ToList();
160 | // get the results
161 | var results = await Task.WhenAll(downloadBookCoverTasks);
162 | // return results that aren't null
163 | return results.Where(r => r != null);
164 | }
165 |
166 | ///
167 | /// Download book cover without cancellation support
168 | ///
169 | private async Task DownloadBookCoverWithoutCancellationSupportAsync(
170 | HttpClient httpClient,
171 | string bookCoverUrl)
172 | {
173 | var response = await httpClient
174 | .GetAsync(bookCoverUrl);
175 |
176 | if (response.IsSuccessStatusCode)
177 | {
178 | var bookCover = JsonSerializer.Deserialize(
179 | await response.Content.ReadAsStringAsync(),
180 | new JsonSerializerOptions()
181 | { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
182 | return bookCover;
183 | }
184 |
185 | return null;
186 | }
187 |
188 | ///
189 | /// Download book cover with cancellation support
190 | ///
191 | private async Task DownloadBookCoverAsync(
192 | HttpClient httpClient,
193 | string bookCoverUrl,
194 | CancellationToken cancellationToken)
195 | {
196 | var response = await httpClient
197 | .GetAsync(bookCoverUrl, cancellationToken);
198 |
199 | if (response.IsSuccessStatusCode)
200 | {
201 | var bookCover = JsonSerializer.Deserialize(
202 | await response.Content.ReadAsStringAsync(cancellationToken),
203 | new JsonSerializerOptions()
204 | { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
205 | return bookCover;
206 | }
207 |
208 | _cancellationTokenSource.Cancel();
209 |
210 | return null;
211 | }
212 |
213 | public IAsyncEnumerable GetBooksAsAsyncEnumerable()
214 | {
215 | return _context.Books.AsAsyncEnumerable();
216 | }
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/Books.Api/Services/IBooksRepository.cs:
--------------------------------------------------------------------------------
1 | using Books.Api.Entities;
2 | using Books.Api.ExternalModels;
3 |
4 | namespace Books.Api.Services
5 | {
6 | public interface IBooksRepository
7 | {
8 | Task GetBookAsync(Guid id);
9 | void AddBook(Book bookToAdd);
10 | Task SaveChangesAsync();
11 | Task> GetBookCoversWithoutCancellationWithForEachAsync(Guid bookId);
12 | Task> GetBookCoversWithoutCancellationAsync(Guid bookId);
13 | Task> GetBookCoversAsync(Guid bookId);
14 | IAsyncEnumerable GetBooksAsAsyncEnumerable();
15 |
16 | }
17 | }
--------------------------------------------------------------------------------
/Books.Api/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Books.Api/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning"
5 | }
6 | },
7 | "AllowedHosts": "*",
8 | "ConnectionStrings": {
9 | "BooksDBConnectionString": "Data Source=Books.db"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Books.Legacy/Books.Legacy.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard21
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Books.Legacy/ComplicatedPageCalculator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Threading;
4 |
5 | namespace Books.Legacy
6 | {
7 | public class ComplicatedPageCalculator
8 | {
9 | ///
10 | /// Full CPU load for 5 seconds
11 | ///
12 | public int CalculateBookPages(Guid bookId)
13 | {
14 | var watch = new Stopwatch();
15 | watch.Start();
16 | while (true)
17 | {
18 | if (watch.ElapsedMilliseconds > 5000)
19 | {
20 | break;
21 | }
22 | }
23 |
24 | return 42;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Books.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27428.2027
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Books.Api", "Books.Api\Books.Api.csproj", "{E3710F56-292E-4E9E-B01A-89F1ABDFA9EE}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Books.Legacy", "Books.Legacy\Books.Legacy.csproj", "{78AA6655-AECA-4B1A-BD3F-BE75C1C664CD}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BookCovers.API", "BookCovers.API\BookCovers.API.csproj", "{452A54A3-9213-4D97-9BD7-592302463242}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {E3710F56-292E-4E9E-B01A-89F1ABDFA9EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {E3710F56-292E-4E9E-B01A-89F1ABDFA9EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {E3710F56-292E-4E9E-B01A-89F1ABDFA9EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {E3710F56-292E-4E9E-B01A-89F1ABDFA9EE}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {78AA6655-AECA-4B1A-BD3F-BE75C1C664CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {78AA6655-AECA-4B1A-BD3F-BE75C1C664CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {78AA6655-AECA-4B1A-BD3F-BE75C1C664CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {78AA6655-AECA-4B1A-BD3F-BE75C1C664CD}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {452A54A3-9213-4D97-9BD7-592302463242}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {452A54A3-9213-4D97-9BD7-592302463242}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {452A54A3-9213-4D97-9BD7-592302463242}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {452A54A3-9213-4D97-9BD7-592302463242}.Release|Any CPU.Build.0 = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {9914921E-DFAC-4FC5-A641-EE317C79DCAD}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Kevin Dockx
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 | # Best Practices for Building Async APIs with ASP.NET Core
2 | Demo code for my "Best Practices for Building Async APIs with ASP.NET Core" session. Updated to .NET 6 with new samples, including IAsyncEnumerable sample (4/2022).
3 |
4 | [Slides can be found here](https://1drv.ms/p/s!ApreYKXHsE4LnZcWmakRejwdsAtH3w)
5 |
--------------------------------------------------------------------------------