├── .gitignore ├── README.md ├── SuperNote.Application.Tests ├── GlobalUsings.cs ├── Notes │ └── QueryHandlers │ │ └── GetNoteByIdQueryHandlerTests.cs └── SuperNote.Application.Tests.csproj ├── SuperNote.Application ├── Abstractions │ ├── Commands │ │ ├── ICommand.cs │ │ └── ICommandHandler.cs │ └── Queries │ │ ├── IQuery.cs │ │ └── IQueryHandler.cs ├── ApplicationServices.cs ├── Notes │ ├── Commands │ │ └── CreateNote │ │ │ ├── CreateNoteCommand.cs │ │ │ └── CreateNoteCommandHandler.cs │ ├── Events │ │ └── NoteCreatedDomainEventHandler.cs │ └── Queries │ │ ├── GetNoteById │ │ ├── GetNoteByIdQuery.cs │ │ ├── GetNoteByIdQueryHandler.cs │ │ └── NoteDto.cs │ │ └── GetNotesList │ │ ├── GetNotesListQuery.cs │ │ ├── GetNotesListQueryHandler.cs │ │ └── NotesListItemDto.cs └── SuperNote.Application.csproj ├── SuperNote.DataAccess ├── DataAccess │ ├── Repository.cs │ ├── SuperNoteContext.cs │ └── UnitOfWork.cs ├── DataAccessServices.cs ├── Migrations │ ├── 20240205155155_Initial.Designer.cs │ ├── 20240205155155_Initial.cs │ └── SuperNoteContextModelSnapshot.cs ├── Notes │ ├── NoteEntityTypeConfiguration.cs │ └── NotesRepository.cs └── SuperNote.DataAccess.csproj ├── SuperNote.Domain.Tests ├── GlobalUsings.cs ├── Notes │ └── NoteTextTests.cs └── SuperNote.Domain.Tests.csproj ├── SuperNote.Domain ├── Abstractions │ ├── Aggregates │ │ └── AggregateRoot.cs │ ├── DataAccess │ │ ├── IRepository.cs │ │ └── IUnitOfWork.cs │ ├── DomainEvents │ │ └── IDomainEvent.cs │ └── ErrorHandling │ │ ├── DomainError.cs │ │ └── ErrorType.cs ├── DomainServices.cs ├── Notes │ ├── INotesRepository.cs │ ├── Note.cs │ ├── NoteCreatedDomainEvent.cs │ ├── NoteErrors.cs │ ├── NoteId.cs │ ├── NoteNotFoundError.cs │ ├── NoteText.cs │ └── NoteTextIsEmpty.cs └── SuperNote.Domain.csproj ├── SuperNote.WebApi ├── AssemblyReferences.cs ├── Endpoints │ ├── ApiRoutes.cs │ └── Notes │ │ ├── Create │ │ ├── Create.cs │ │ ├── CreateNoteRequest.cs │ │ └── CreateNoteResponse.cs │ │ ├── GetById │ │ ├── GetById.cs │ │ └── GetNoteByIdRequest.cs │ │ └── GetList │ │ └── GetList.cs ├── Extensions │ └── ResponseExtensions.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── SuperNote.WebApi.csproj └── appsettings.json └── super-note.sln /.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 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Rider cache/options directory 37 | .idea/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # StyleCop 67 | StyleCopReport.xml 68 | 69 | # Files built by Visual Studio 70 | *_i.c 71 | *_p.c 72 | *_h.h 73 | *.ilk 74 | *.meta 75 | *.obj 76 | *.iobj 77 | *.pch 78 | *.pdb 79 | *.ipdb 80 | *.pgc 81 | *.pgd 82 | *.rsp 83 | *.sbr 84 | *.tlb 85 | *.tli 86 | *.tlh 87 | *.tmp 88 | *.tmp_proj 89 | *_wpftmp.csproj 90 | *.log 91 | *.vspscc 92 | *.vssscc 93 | .builds 94 | *.pidb 95 | *.svclog 96 | *.scc 97 | 98 | # Chutzpah Test files 99 | _Chutzpah* 100 | 101 | # Visual C++ cache files 102 | ipch/ 103 | *.aps 104 | *.ncb 105 | *.opendb 106 | *.opensdf 107 | *.sdf 108 | *.cachefile 109 | *.VC.db 110 | *.VC.VC.opendb 111 | 112 | # Visual Studio profiler 113 | *.psess 114 | *.vsp 115 | *.vspx 116 | *.sap 117 | 118 | # Visual Studio Trace Files 119 | *.e2e 120 | 121 | # TFS 2012 Local Workspace 122 | $tf/ 123 | 124 | # Guidance Automation Toolkit 125 | *.gpState 126 | 127 | # ReSharper is a .NET coding add-in 128 | _ReSharper*/ 129 | *.[Rr]e[Ss]harper 130 | *.DotSettings.user 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | # Ionide (cross platform F# VS Code tools) working folder 352 | .ionide/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # .NET 8 REST API Project That Implements Clean Architecture and Domain-Driven Design. 2 | 3 | ## Give a Star! :star: 4 | If the project has been useful to you, please give it a star. Thank you! 5 | 6 | ## Description 7 | SuperNote is the initial version of a note-taking app (such as [OneNote](https://www.onenote.com/) or [Notion](https://www.notion.so/)). There is no front-end at this time. The project contains a few REST API endpoints: 8 | 9 | ![image](https://github.com/sashamarfuttech/super-note-api/assets/158445722/ade6029e-bc2a-40f3-b371-94c95ed24116) 10 | 11 | Technologies & Libraries: 12 | 1. [ASP.NET Core 8.0](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0) 13 | 3. [Entity Framework Core 8](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew) 14 | 2. [FastEndpoints 5.22.0](https://fast-endpoints.com/) 15 | 4. [MediatR 12.2.0](https://github.com/jbogard/MediatR) 16 | 5. [FluentResults 3.15](https://github.com/altmann/FluentResults) 17 | 6. [Optional 4.0](https://github.com/nlkl/Optional) 18 | 19 | Databases: 20 | 1. [PostgreSQL](https://www.postgresql.org/) 21 | 2. [Entity Framework Core 8 In-Memory Database](https://learn.microsoft.com/en-us/ef/core/providers/in-memory/?tabs=dotnet-core-cli) 22 | 23 | ## How do I run the application? 24 | 1. Check out source code from the repository. 25 | 2. Run **SuperNote.WebApi** project. 26 | 27 | You'll see Swagger. 28 | 29 | By default, the project is configured to use the Entity Framework In Memory database. 30 | So the above two steps will be enough if you want to have a quick play around with the app. 31 | 32 | If you want to use a real database, then 33 | 1. Create empty database. 34 | 2. Go to the **appsettings.json** file and set the connection string in the **Sql:ConnectionString** section. 35 | 3. Run the migrations defined in the **SuperNote.DataAccess** project. 36 | 4. Run **SuperNote.WebApi** project. 37 | 38 | The application uses a PostgreSQL database, but it can be easily changed it in the DataAccessServices.cs file if needed. 39 | 40 | ## Project Structure 41 | 42 | The project implements a clean architecture. Here's its structure: 43 | 44 | ![Screenshot 2024-02-06 193709](https://github.com/sashamarfuttech/super-note-api/assets/158445722/c7b309dc-07b8-42cc-8982-33ec50e90cd1) 45 | 46 | Now let's talk about each project separately. 47 | 48 | **SuperNote.Domain** 49 | 50 | The domain layer contains domain entities, domain events, repository interfaces, domain errors, and other core application logic. 51 | 52 | **SuperNote.Application (Use cases)** 53 | 54 | The application layer implements the SuperNote application use cases using Commands and Queries. 55 | It also implements event handlers for domain events. 56 | 57 | In addition, the application layer is where **abstractions** for caching, messaging, authentication, email notifications, and so on are placed. 58 | 59 | **SuperNote.Infrastructure** 60 | 61 | Currently, the infrastructure layer contains a single project, which is DataAccess. This project implements repositories, migrations, and other things related to data access. 62 | 63 | In addition, the infrastructure layer must implement the caching, messaging, authentication, email notification abstractions that are defined in the application layer. 64 | 65 | **SuperNote.WebApi (Presentation)** 66 | 67 | This is the entry point to the application. It implements a set of REST APIs that clients can use to interact with the application. 68 | 69 | ## Key Patterns 70 | 71 | [REPR Design Pattern](https://deviq.com/design-patterns/repr-design-pattern) 72 | 73 | REPR stands for Request, an Endpoint, and a Response. The pattern enforces the Single Responsibility Principle for your endpoints. The basic idea is that each request is handled by a separate class. 74 | 75 | [Result Pattern](https://github.com/altmann/FluentResults) 76 | 77 | There are two fundamental ways for handling invalid input in your domain: throw an exception or return an object indicating the error. The SuperNote application uses the second approach, returning a Result object from the domain and application layers. You can read about the reasons for choosing one or the other [here](https://enterprisecraftsmanship.com/posts/exceptions-for-flow-control/), [here](https://www.silasreinagel.com/blog/2018/06/18/result-vs-exception/) or [here](https://softwareengineering.stackexchange.com/questions/405038/result-object-vs-throwing-exceptions). 78 | 79 | -------------------------------------------------------------------------------- /SuperNote.Application.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /SuperNote.Application.Tests/Notes/QueryHandlers/GetNoteByIdQueryHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using Optional; 3 | using SuperNote.Application.Notes.Queries.GetNoteById; 4 | using SuperNote.Domain.Notes; 5 | 6 | namespace SuperNote.Application.Tests.Notes.QueryHandlers; 7 | 8 | public class GetNoteByIdQueryHandlerTests 9 | { 10 | private readonly Mock _notesRepositoryMock = new (); 11 | 12 | private readonly GetNoteByIdQueryHandler _sut; 13 | 14 | public GetNoteByIdQueryHandlerTests() 15 | { 16 | _sut = new (_notesRepositoryMock.Object); 17 | } 18 | 19 | [Fact] 20 | public async Task Get_note_by_id_returns_error_when_note_does_not_exist() 21 | { 22 | // Arrange 23 | _notesRepositoryMock 24 | .Setup(x => x.GetByIdAsync(It.IsAny())) 25 | .ReturnsAsync(Option.None()); 26 | 27 | // Act 28 | var result = await _sut.Handle(new GetNoteByIdQuery(It.IsAny()), default); 29 | 30 | // Assert 31 | var contains = result.Errors.Contains(NoteErrors.NoteNotFound); 32 | Assert.True(contains); 33 | } 34 | 35 | [Fact] 36 | public async Task Get_note_by_id_returns_a_note() 37 | { 38 | // Arrange 39 | var noteText = NoteText.Create("text"); 40 | var note = new Note(noteText.Value, DateTime.Now); 41 | 42 | NoteDto expectedNoteDto = new (note.Id.Value, note.NoteText.Value, note.LastModified); 43 | 44 | _notesRepositoryMock 45 | .Setup(x => x.GetByIdAsync(note.Id)) 46 | .ReturnsAsync(note.Some()); 47 | 48 | // Act 49 | var noteDto = await _sut.Handle(new GetNoteByIdQuery(note.Id), default); 50 | 51 | // Assert 52 | Assert.Equal(expectedNoteDto, noteDto.Value); 53 | } 54 | } -------------------------------------------------------------------------------- /SuperNote.Application.Tests/SuperNote.Application.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /SuperNote.Application/Abstractions/Commands/ICommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace SuperNote.Application.Abstractions.Commands; 4 | 5 | public interface ICommand : IRequest 6 | { 7 | } 8 | 9 | public interface ICommand : IRequest 10 | { 11 | } -------------------------------------------------------------------------------- /SuperNote.Application/Abstractions/Commands/ICommandHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace SuperNote.Application.Abstractions.Commands; 4 | 5 | public interface ICommandHandler : 6 | IRequestHandler where TCommand : ICommand 7 | { 8 | } 9 | 10 | public interface ICommandHandler 11 | : IRequestHandler where TCommand : IRequest 12 | { 13 | } -------------------------------------------------------------------------------- /SuperNote.Application/Abstractions/Queries/IQuery.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace SuperNote.Application.Abstractions.Queries; 4 | 5 | public interface IQuery : IRequest 6 | { 7 | } -------------------------------------------------------------------------------- /SuperNote.Application/Abstractions/Queries/IQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace SuperNote.Application.Abstractions.Queries; 4 | 5 | public interface IQueryHandler : 6 | IRequestHandler where TQuery : IQuery 7 | { 8 | } -------------------------------------------------------------------------------- /SuperNote.Application/ApplicationServices.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace SuperNote.Application; 4 | 5 | public static class ApplicationServices 6 | { 7 | public static IServiceCollection AddApplicationServices(this IServiceCollection services) 8 | { 9 | return services; 10 | } 11 | } -------------------------------------------------------------------------------- /SuperNote.Application/Notes/Commands/CreateNote/CreateNoteCommand.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using SuperNote.Application.Abstractions.Commands; 3 | 4 | namespace SuperNote.Application.Notes.Commands.CreateNote; 5 | 6 | public record CreateNoteCommand(string Text) : ICommand>; -------------------------------------------------------------------------------- /SuperNote.Application/Notes/Commands/CreateNote/CreateNoteCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using SuperNote.Application.Abstractions.Commands; 3 | using SuperNote.Domain.Abstractions.DataAccess; 4 | using SuperNote.Domain.Notes; 5 | 6 | namespace SuperNote.Application.Notes.Commands.CreateNote; 7 | 8 | public class CreateNoteCommandHandler : ICommandHandler> 9 | { 10 | private readonly IUnitOfWork _unitOfWork; 11 | private readonly INotesRepository _notesRepository; 12 | private readonly TimeProvider _timeProvider; 13 | 14 | public CreateNoteCommandHandler( 15 | IUnitOfWork unitOfWork, 16 | INotesRepository notesRepository, 17 | TimeProvider timeProvider) 18 | { 19 | _unitOfWork = unitOfWork; 20 | _notesRepository = notesRepository; 21 | _timeProvider = timeProvider; 22 | } 23 | 24 | public async Task> Handle(CreateNoteCommand request, CancellationToken cancellationToken) 25 | { 26 | var noteText = NoteText.Create(request.Text); 27 | 28 | if (noteText.IsFailed) 29 | { 30 | return Result.Fail(noteText.Errors); 31 | } 32 | 33 | Note note = new (noteText.Value, _timeProvider.GetUtcNow().UtcDateTime); 34 | 35 | _notesRepository.Add(note); 36 | 37 | await _unitOfWork.SaveChangesAsync(cancellationToken); 38 | 39 | return Result.Ok(note.Id.Value); 40 | } 41 | } -------------------------------------------------------------------------------- /SuperNote.Application/Notes/Events/NoteCreatedDomainEventHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.Extensions.Logging; 3 | using SuperNote.Domain.Notes; 4 | 5 | namespace SuperNote.Application.Notes.Events; 6 | 7 | public sealed class NoteCreatedDomainEventHandler : INotificationHandler 8 | { 9 | private readonly ILogger _logger; 10 | 11 | public NoteCreatedDomainEventHandler(ILogger logger) 12 | { 13 | _logger = logger; 14 | } 15 | 16 | public async Task Handle(NoteCreatedDomainEvent notification, CancellationToken cancellationToken) 17 | { 18 | _logger.LogWarning("NoteCreatedDomainEventHandler is intentionally left blank."); 19 | 20 | await Task.CompletedTask; 21 | } 22 | } -------------------------------------------------------------------------------- /SuperNote.Application/Notes/Queries/GetNoteById/GetNoteByIdQuery.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using SuperNote.Application.Abstractions.Queries; 3 | using SuperNote.Domain.Notes; 4 | 5 | namespace SuperNote.Application.Notes.Queries.GetNoteById; 6 | 7 | public record GetNoteByIdQuery(NoteId Id) : IQuery>; -------------------------------------------------------------------------------- /SuperNote.Application/Notes/Queries/GetNoteById/GetNoteByIdQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using Optional; 3 | using Optional.Unsafe; 4 | using SuperNote.Application.Abstractions.Queries; 5 | using SuperNote.Domain.Notes; 6 | 7 | namespace SuperNote.Application.Notes.Queries.GetNoteById; 8 | 9 | public class GetNoteByIdQueryHandler : IQueryHandler> 10 | { 11 | private readonly INotesRepository _notesRepository; 12 | 13 | public GetNoteByIdQueryHandler(INotesRepository notesRepository) 14 | => _notesRepository = notesRepository; 15 | 16 | public async Task> Handle(GetNoteByIdQuery request, CancellationToken cancellationToken) 17 | { 18 | Option noteOption = await _notesRepository.GetByIdAsync(request.Id); 19 | 20 | if (!noteOption.HasValue) 21 | { 22 | return NoteErrors.NoteNotFound; 23 | } 24 | 25 | var note = noteOption.ValueOrDefault(); 26 | 27 | NoteDto noteDto = new (note.Id.Value, note.NoteText.Value, note.LastModified); 28 | 29 | return Result.Ok(noteDto); 30 | } 31 | } -------------------------------------------------------------------------------- /SuperNote.Application/Notes/Queries/GetNoteById/NoteDto.cs: -------------------------------------------------------------------------------- 1 | namespace SuperNote.Application.Notes.Queries.GetNoteById; 2 | 3 | public record NoteDto( 4 | Guid Id, 5 | string Text, 6 | DateTime LastModified); -------------------------------------------------------------------------------- /SuperNote.Application/Notes/Queries/GetNotesList/GetNotesListQuery.cs: -------------------------------------------------------------------------------- 1 | using SuperNote.Application.Abstractions.Queries; 2 | 3 | namespace SuperNote.Application.Notes.Queries.GetNotesList; 4 | 5 | public record GetNotesListQuery() : IQuery>; -------------------------------------------------------------------------------- /SuperNote.Application/Notes/Queries/GetNotesList/GetNotesListQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using SuperNote.Application.Abstractions.Queries; 2 | using SuperNote.Domain.Notes; 3 | 4 | namespace SuperNote.Application.Notes.Queries.GetNotesList; 5 | 6 | public class GetNotesListQueryHandler : IQueryHandler> 7 | { 8 | private readonly INotesRepository _notesRepository; 9 | 10 | public GetNotesListQueryHandler(INotesRepository notesRepository) => _notesRepository = notesRepository; 11 | 12 | public async Task> Handle(GetNotesListQuery request, CancellationToken cancellationToken) 13 | { 14 | var notes = await _notesRepository.GetALl(); 15 | 16 | return notes 17 | .Select(n => new NotesListItemDto(n.Id.Value, n.NoteText.Value, n.LastModified)) 18 | .ToList(); 19 | } 20 | } -------------------------------------------------------------------------------- /SuperNote.Application/Notes/Queries/GetNotesList/NotesListItemDto.cs: -------------------------------------------------------------------------------- 1 | namespace SuperNote.Application.Notes.Queries.GetNotesList; 2 | 3 | public record NotesListItemDto( 4 | Guid Id, 5 | string Text, 6 | DateTime LastModified); -------------------------------------------------------------------------------- /SuperNote.Application/SuperNote.Application.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SuperNote.DataAccess/DataAccess/Repository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using SuperNote.Domain.Abstractions.Aggregates; 3 | using SuperNote.Domain.Abstractions.DataAccess; 4 | 5 | namespace SuperNote.DataAccess.DataAccess; 6 | 7 | public abstract class Repository : IRepository 8 | where TEntity : AggregateRoot 9 | { 10 | protected readonly SuperNoteContext DbContext; 11 | 12 | protected Repository(SuperNoteContext dbContext) => DbContext = dbContext; 13 | 14 | public async Task> GetALl() 15 | => await DbContext.Set().ToListAsync(); 16 | 17 | public void Add(TEntity entity) 18 | => DbContext.Set().Add(entity); 19 | 20 | public void Update(TEntity entity) 21 | => DbContext.Set().Update(entity); 22 | 23 | public void Remove(TEntity entity) 24 | => DbContext.Set().Remove(entity); 25 | } -------------------------------------------------------------------------------- /SuperNote.DataAccess/DataAccess/SuperNoteContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using SuperNote.Domain.Notes; 3 | 4 | namespace SuperNote.DataAccess.DataAccess; 5 | 6 | public class SuperNoteContext : DbContext 7 | { 8 | public SuperNoteContext( 9 | DbContextOptions options) 10 | : base(options) 11 | { 12 | } 13 | 14 | public DbSet Notes { get; set; } 15 | 16 | protected override void OnModelCreating(ModelBuilder modelBuilder) 17 | { 18 | modelBuilder.ApplyConfigurationsFromAssembly(typeof(SuperNoteContext).Assembly); 19 | } 20 | } -------------------------------------------------------------------------------- /SuperNote.DataAccess/DataAccess/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using SuperNote.Domain.Abstractions.Aggregates; 3 | using SuperNote.Domain.Abstractions.DataAccess; 4 | using SuperNote.Domain.Abstractions.DomainEvents; 5 | 6 | namespace SuperNote.DataAccess.DataAccess; 7 | 8 | public class UnitOfWork : IUnitOfWork 9 | { 10 | private readonly IPublisher _publisher; 11 | private readonly SuperNoteContext _context; 12 | 13 | public UnitOfWork( 14 | IPublisher publisher, 15 | SuperNoteContext context) 16 | { 17 | _publisher = publisher; 18 | _context = context; 19 | } 20 | 21 | public async Task SaveChangesAsync(CancellationToken cancellationToken = default) 22 | { 23 | var domainEntities = GetDomainEntities(_context); 24 | var domainEvents = GetDomainEvents(domainEntities); 25 | 26 | await _context.SaveChangesAsync(cancellationToken); 27 | 28 | foreach (var domainEvent in domainEvents) 29 | { 30 | await _publisher.Publish(domainEvent, cancellationToken); 31 | } 32 | 33 | domainEntities.ForEach(d => d.ClearDomainEvents()); 34 | 35 | List GetDomainEntities(SuperNoteContext context) => 36 | context 37 | .ChangeTracker 38 | .Entries() 39 | .Select(e => e.Entity) 40 | .ToList(); 41 | 42 | List GetDomainEvents(List entities) => 43 | entities 44 | .Where(e => e.DomainEvents.Any()) 45 | .SelectMany(e => e.DomainEvents) 46 | .ToList(); 47 | } 48 | } -------------------------------------------------------------------------------- /SuperNote.DataAccess/DataAccessServices.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using SuperNote.DataAccess.DataAccess; 4 | using SuperNote.DataAccess.Notes; 5 | using SuperNote.Domain.Abstractions.DataAccess; 6 | using SuperNote.Domain.Notes; 7 | 8 | namespace SuperNote.DataAccess; 9 | 10 | public static class DataAccessServices 11 | { 12 | private const string InMemoryDatabaseName = "SuperNoteInMemoryDatabase"; 13 | 14 | public static IServiceCollection AddDataAccessServices(this IServiceCollection services, string? connectionString) 15 | { 16 | if (!string.IsNullOrEmpty(connectionString)) 17 | { 18 | services.AddDbContext(options => options.UseNpgsql(connectionString)); 19 | } 20 | else 21 | { 22 | services.AddDbContext(options => options.UseInMemoryDatabase(InMemoryDatabaseName)); 23 | } 24 | 25 | services.AddScoped(); 26 | services.AddScoped(); 27 | 28 | return services; 29 | } 30 | } -------------------------------------------------------------------------------- /SuperNote.DataAccess/Migrations/20240205155155_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | using SuperNote.DataAccess.DataAccess; 9 | 10 | #nullable disable 11 | 12 | namespace SuperNote.DataAccess.Migrations 13 | { 14 | [DbContext(typeof(SuperNoteContext))] 15 | [Migration("20240205155155_Initial")] 16 | partial class Initial 17 | { 18 | /// 19 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 20 | { 21 | #pragma warning disable 612, 618 22 | modelBuilder 23 | .HasAnnotation("ProductVersion", "8.0.1") 24 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 25 | 26 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 27 | 28 | modelBuilder.Entity("SuperNote.Domain.Notes.Note", b => 29 | { 30 | b.Property("Id") 31 | .HasColumnType("uuid"); 32 | 33 | b.Property("LastModified") 34 | .HasColumnType("timestamp with time zone"); 35 | 36 | b.Property("NoteText") 37 | .IsRequired() 38 | .HasColumnType("text"); 39 | 40 | b.HasKey("Id"); 41 | 42 | b.ToTable("Notes"); 43 | }); 44 | #pragma warning restore 612, 618 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SuperNote.DataAccess/Migrations/20240205155155_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace SuperNote.DataAccess.Migrations 7 | { 8 | /// 9 | public partial class Initial : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "Notes", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "uuid", nullable: false), 19 | NoteText = table.Column(type: "text", nullable: false), 20 | LastModified = table.Column(type: "timestamp with time zone", nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Notes", x => x.Id); 25 | }); 26 | } 27 | 28 | /// 29 | protected override void Down(MigrationBuilder migrationBuilder) 30 | { 31 | migrationBuilder.DropTable( 32 | name: "Notes"); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SuperNote.DataAccess/Migrations/SuperNoteContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 7 | using SuperNote.DataAccess.DataAccess; 8 | 9 | #nullable disable 10 | 11 | namespace SuperNote.DataAccess.Migrations 12 | { 13 | [DbContext(typeof(SuperNoteContext))] 14 | partial class SuperNoteContextModelSnapshot : ModelSnapshot 15 | { 16 | protected override void BuildModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "8.0.1") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 22 | 23 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 24 | 25 | modelBuilder.Entity("SuperNote.Domain.Notes.Note", b => 26 | { 27 | b.Property("Id") 28 | .HasColumnType("uuid"); 29 | 30 | b.Property("LastModified") 31 | .HasColumnType("timestamp with time zone"); 32 | 33 | b.Property("NoteText") 34 | .IsRequired() 35 | .HasColumnType("text"); 36 | 37 | b.HasKey("Id"); 38 | 39 | b.ToTable("Notes"); 40 | }); 41 | #pragma warning restore 612, 618 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SuperNote.DataAccess/Notes/NoteEntityTypeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using SuperNote.Domain.Notes; 4 | 5 | namespace SuperNote.DataAccess.Notes; 6 | 7 | internal class NoteEntityTypeConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder 12 | .HasKey(b => b.Id); 13 | 14 | builder 15 | .Property(e => e.Id) 16 | .HasConversion(id => id.Value, value => new NoteId(value)); 17 | 18 | builder 19 | .Property(e => e.NoteText) 20 | .HasConversion(e => e.Value, val => NoteText.Create(val).Value); 21 | 22 | builder.Property(x => x.LastModified); 23 | } 24 | } -------------------------------------------------------------------------------- /SuperNote.DataAccess/Notes/NotesRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Optional; 3 | using SuperNote.DataAccess.DataAccess; 4 | using SuperNote.Domain.Notes; 5 | 6 | namespace SuperNote.DataAccess.Notes; 7 | 8 | public class NotesRepository : Repository, INotesRepository 9 | { 10 | public NotesRepository(SuperNoteContext dbContext) 11 | : base(dbContext) 12 | { 13 | } 14 | 15 | public async Task> GetByIdAsync(NoteId noteId) 16 | { 17 | var note = await DbContext.Notes.SingleOrDefaultAsync(n => n.Id == noteId); 18 | return note.SomeNotNull(); 19 | } 20 | } -------------------------------------------------------------------------------- /SuperNote.DataAccess/SuperNote.DataAccess.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /SuperNote.Domain.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /SuperNote.Domain.Tests/Notes/NoteTextTests.cs: -------------------------------------------------------------------------------- 1 | using SuperNote.Domain.Notes; 2 | 3 | namespace SuperNote.Domain.Tests.Notes; 4 | 5 | public class NoteTextTests 6 | { 7 | [Theory] 8 | [InlineData(null)] 9 | [InlineData("")] 10 | [InlineData(" ")] 11 | public void Note_text_is_not_created_without_data(string data) 12 | { 13 | // Act 14 | var noteText = NoteText.Create(data); 15 | 16 | // Assert 17 | Assert.True(noteText.IsFailed); 18 | } 19 | 20 | [Fact] 21 | public void Note_text_is_created() 22 | { 23 | // Act 24 | var noteText = NoteText.Create("I need to schedule a meeting with John Doe"); 25 | 26 | // Assert 27 | Assert.True(noteText.IsSuccess); 28 | } 29 | } -------------------------------------------------------------------------------- /SuperNote.Domain.Tests/SuperNote.Domain.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /SuperNote.Domain/Abstractions/Aggregates/AggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using SuperNote.Domain.Abstractions.DomainEvents; 2 | 3 | namespace SuperNote.Domain.Abstractions.Aggregates; 4 | 5 | public abstract class AggregateRoot 6 | { 7 | private readonly List _domainEvents = new(); 8 | public IReadOnlyList DomainEvents => _domainEvents; 9 | 10 | protected void RaiseDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent); 11 | 12 | public void ClearDomainEvents() => _domainEvents.Clear(); 13 | } -------------------------------------------------------------------------------- /SuperNote.Domain/Abstractions/DataAccess/IRepository.cs: -------------------------------------------------------------------------------- 1 | using SuperNote.Domain.Abstractions.Aggregates; 2 | 3 | namespace SuperNote.Domain.Abstractions.DataAccess; 4 | 5 | public interface IRepository where TEntity : AggregateRoot 6 | { 7 | Task> GetALl(); 8 | void Add(TEntity entity); 9 | void Update(TEntity entity); 10 | void Remove(TEntity entity); 11 | } -------------------------------------------------------------------------------- /SuperNote.Domain/Abstractions/DataAccess/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | namespace SuperNote.Domain.Abstractions.DataAccess; 2 | 3 | public interface IUnitOfWork 4 | { 5 | Task SaveChangesAsync(CancellationToken cancellationToken = default); 6 | } -------------------------------------------------------------------------------- /SuperNote.Domain/Abstractions/DomainEvents/IDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace SuperNote.Domain.Abstractions.DomainEvents; 4 | 5 | public interface IDomainEvent : INotification 6 | { 7 | } -------------------------------------------------------------------------------- /SuperNote.Domain/Abstractions/ErrorHandling/DomainError.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | 3 | namespace SuperNote.Domain.Abstractions.ErrorHandling; 4 | 5 | public class DomainError : Error 6 | { 7 | public const string ErrorCodeLiteral = "ErrorCode"; 8 | 9 | protected DomainError(string message, string code) 10 | : base(message) 11 | { 12 | WithMetadata(ErrorCodeLiteral, code); 13 | } 14 | } -------------------------------------------------------------------------------- /SuperNote.Domain/Abstractions/ErrorHandling/ErrorType.cs: -------------------------------------------------------------------------------- 1 | namespace SuperNote.Domain.Abstractions.ErrorHandling; 2 | 3 | public enum ErrorType 4 | { 5 | NotFound = 0, 6 | InvalidData = 1 7 | } -------------------------------------------------------------------------------- /SuperNote.Domain/DomainServices.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace SuperNote.Domain; 4 | 5 | public static class DomainServices 6 | { 7 | public static IServiceCollection AddDomainServices(this IServiceCollection services) 8 | { 9 | return services; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SuperNote.Domain/Notes/INotesRepository.cs: -------------------------------------------------------------------------------- 1 | using Optional; 2 | using SuperNote.Domain.Abstractions.DataAccess; 3 | 4 | namespace SuperNote.Domain.Notes; 5 | 6 | public interface INotesRepository : IRepository 7 | { 8 | Task> GetByIdAsync(NoteId noteId); 9 | } -------------------------------------------------------------------------------- /SuperNote.Domain/Notes/Note.cs: -------------------------------------------------------------------------------- 1 | using SuperNote.Domain.Abstractions.Aggregates; 2 | 3 | namespace SuperNote.Domain.Notes; 4 | 5 | public class Note : AggregateRoot 6 | { 7 | /// 8 | /// Required by Entity Framework 9 | /// 10 | private Note() 11 | { 12 | } 13 | 14 | public Note(NoteText noteText, DateTime lastModified) 15 | { 16 | Id = NoteId.New(); 17 | NoteText = noteText; 18 | LastModified = lastModified; 19 | 20 | RaiseDomainEvent(new NoteCreatedDomainEvent(Id)); 21 | } 22 | 23 | public NoteId Id { get; } 24 | public NoteText NoteText { get; } 25 | public DateTime LastModified { get; } 26 | } -------------------------------------------------------------------------------- /SuperNote.Domain/Notes/NoteCreatedDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using SuperNote.Domain.Abstractions.DomainEvents; 2 | 3 | namespace SuperNote.Domain.Notes; 4 | 5 | public record NoteCreatedDomainEvent(NoteId NoteId) : IDomainEvent; -------------------------------------------------------------------------------- /SuperNote.Domain/Notes/NoteErrors.cs: -------------------------------------------------------------------------------- 1 | namespace SuperNote.Domain.Notes; 2 | 3 | public static class NoteErrors 4 | { 5 | public static readonly NoteNotFoundError NoteNotFound = new ("note.not.found", "Note not found."); 6 | public static readonly NoteTextIsEmptyError TheNoteIsEmpty = new ("the.note.is.empty", "The note is empty."); 7 | } -------------------------------------------------------------------------------- /SuperNote.Domain/Notes/NoteId.cs: -------------------------------------------------------------------------------- 1 | namespace SuperNote.Domain.Notes; 2 | 3 | public record struct NoteId(Guid Value) 4 | { 5 | public static NoteId New() => new NoteId(Guid.NewGuid()); 6 | } -------------------------------------------------------------------------------- /SuperNote.Domain/Notes/NoteNotFoundError.cs: -------------------------------------------------------------------------------- 1 | using SuperNote.Domain.Abstractions.ErrorHandling; 2 | 3 | namespace SuperNote.Domain.Notes; 4 | 5 | public class NoteNotFoundError : DomainError 6 | { 7 | public NoteNotFoundError(string code, string message) 8 | : base(message, code) 9 | { 10 | WithMetadata(nameof(ErrorType), ErrorType.NotFound); 11 | } 12 | } -------------------------------------------------------------------------------- /SuperNote.Domain/Notes/NoteText.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | 3 | namespace SuperNote.Domain.Notes; 4 | 5 | public sealed record NoteText 6 | { 7 | public string Value { get; } 8 | 9 | private NoteText(string value) => Value = value; 10 | 11 | public static Result Create(string value) 12 | { 13 | if (string.IsNullOrWhiteSpace(value)) 14 | { 15 | return NoteErrors.TheNoteIsEmpty; 16 | } 17 | 18 | return Result.Ok(new NoteText(value)); 19 | } 20 | } -------------------------------------------------------------------------------- /SuperNote.Domain/Notes/NoteTextIsEmpty.cs: -------------------------------------------------------------------------------- 1 | using SuperNote.Domain.Abstractions.ErrorHandling; 2 | 3 | namespace SuperNote.Domain.Notes; 4 | 5 | public class NoteTextIsEmptyError : DomainError 6 | { 7 | public NoteTextIsEmptyError(string code, string message) 8 | : base(message, code) 9 | { 10 | WithMetadata(nameof(ErrorType), ErrorType.InvalidData); 11 | } 12 | } -------------------------------------------------------------------------------- /SuperNote.Domain/SuperNote.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 12 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /SuperNote.WebApi/AssemblyReferences.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using SuperNote.Application; 3 | 4 | namespace SuperNote.WebApi; 5 | 6 | public static class AssemblyReferences 7 | { 8 | public static readonly Assembly WebApi = typeof(Program).Assembly; 9 | public static readonly Assembly Application = typeof(ApplicationServices).Assembly; 10 | } -------------------------------------------------------------------------------- /SuperNote.WebApi/Endpoints/ApiRoutes.cs: -------------------------------------------------------------------------------- 1 | namespace SuperNote.WebApi.Endpoints; 2 | 3 | public static class ApiRoutes 4 | { 5 | public static class Notes 6 | { 7 | public const string GroupName = nameof(Notes); 8 | 9 | public const string Create = $"/notes/"; 10 | public const string GetList = $"/notes/"; 11 | public const string GetById = $"/notes/{{noteId:guid}}/"; 12 | } 13 | } -------------------------------------------------------------------------------- /SuperNote.WebApi/Endpoints/Notes/Create/Create.cs: -------------------------------------------------------------------------------- 1 | using FastEndpoints; 2 | using MediatR; 3 | using SuperNote.Application.Notes.Commands.CreateNote; 4 | using SuperNote.WebApi.Extensions; 5 | 6 | namespace SuperNote.WebApi.Endpoints.Notes.Create; 7 | 8 | public class Create(IMediator mediator) : Endpoint 9 | { 10 | public override void Configure() 11 | { 12 | AllowAnonymous(); 13 | 14 | Post(ApiRoutes.Notes.Create); 15 | 16 | Summary(s => 17 | { 18 | s.Summary = "Create a new note."; 19 | }); 20 | } 21 | 22 | public override async Task HandleAsync(CreateNoteRequest req, CancellationToken ct) 23 | { 24 | var result = await mediator.Send(new CreateNoteCommand(req.Text), ct); 25 | 26 | await (result.IsSuccess switch 27 | { 28 | true => SendOkAsync(new CreateNoteResponse(result.Value), ct), 29 | false => this.SendProblemDetailsResponse(result, ct) 30 | }); 31 | } 32 | } -------------------------------------------------------------------------------- /SuperNote.WebApi/Endpoints/Notes/Create/CreateNoteRequest.cs: -------------------------------------------------------------------------------- 1 | namespace SuperNote.WebApi.Endpoints.Notes.Create; 2 | 3 | public record CreateNoteRequest(string Text); -------------------------------------------------------------------------------- /SuperNote.WebApi/Endpoints/Notes/Create/CreateNoteResponse.cs: -------------------------------------------------------------------------------- 1 | namespace SuperNote.WebApi.Endpoints.Notes.Create; 2 | 3 | public record CreateNoteResponse(Guid Id); -------------------------------------------------------------------------------- /SuperNote.WebApi/Endpoints/Notes/GetById/GetById.cs: -------------------------------------------------------------------------------- 1 | using FastEndpoints; 2 | using MediatR; 3 | using SuperNote.Application.Notes.Queries.GetNoteById; 4 | using SuperNote.Domain.Notes; 5 | using SuperNote.WebApi.Extensions; 6 | 7 | namespace SuperNote.WebApi.Endpoints.Notes.GetById; 8 | 9 | public class GetById(IMediator mediator) : Endpoint 10 | { 11 | public override void Configure() 12 | { 13 | AllowAnonymous(); 14 | 15 | Get(ApiRoutes.Notes.GetById); 16 | 17 | Summary(s => 18 | { 19 | s.Summary = "Retrieve a note by its ID."; 20 | }); 21 | } 22 | 23 | public override async Task HandleAsync(GetNoteByIdRequest req, CancellationToken ct) 24 | { 25 | var note = await mediator.Send(new GetNoteByIdQuery(new NoteId(req.NoteId)), ct); 26 | 27 | await (note.IsSuccess switch 28 | { 29 | true => SendOkAsync(note.Value, ct), 30 | false => this.SendProblemDetailsResponse(note, ct) 31 | }); 32 | } 33 | } -------------------------------------------------------------------------------- /SuperNote.WebApi/Endpoints/Notes/GetById/GetNoteByIdRequest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace SuperNote.WebApi.Endpoints.Notes.GetById; 4 | 5 | public class GetNoteByIdRequest 6 | { 7 | [FromRoute] 8 | public Guid NoteId { get; set; } 9 | } -------------------------------------------------------------------------------- /SuperNote.WebApi/Endpoints/Notes/GetList/GetList.cs: -------------------------------------------------------------------------------- 1 | using FastEndpoints; 2 | using MediatR; 3 | using SuperNote.Application.Notes.Queries.GetNotesList; 4 | 5 | namespace SuperNote.WebApi.Endpoints.Notes.GetList; 6 | 7 | public class GetList(IMediator mediator) : Endpoint> 8 | { 9 | public override void Configure() 10 | { 11 | AllowAnonymous(); 12 | 13 | Get(ApiRoutes.Notes.GetList); 14 | 15 | Summary(s => 16 | { 17 | s.Summary = "Returning the list of notes."; 18 | }); 19 | } 20 | 21 | public override async Task HandleAsync(EmptyRequest req, CancellationToken ct) 22 | { 23 | var notes = await mediator.Send(new GetNotesListQuery(), ct); 24 | await SendOkAsync(notes, ct); 25 | } 26 | } -------------------------------------------------------------------------------- /SuperNote.WebApi/Extensions/ResponseExtensions.cs: -------------------------------------------------------------------------------- 1 | using FastEndpoints; 2 | using FluentResults; 3 | using FluentValidation.Results; 4 | using SuperNote.Domain.Abstractions.ErrorHandling; 5 | 6 | namespace SuperNote.WebApi.Extensions; 7 | 8 | public static class ResponseExtensions 9 | { 10 | public static async Task SendProblemDetailsResponse( 11 | this IEndpoint ep, 12 | Result result, 13 | CancellationToken ct) 14 | { 15 | if (result.IsSuccess) 16 | { 17 | throw new ArgumentException("The Result object must be in a failed state."); 18 | } 19 | 20 | var errorType = GetErrorType(result.Errors.First()); 21 | 22 | var statusCode = errorType switch 23 | { 24 | ErrorType.NotFound => StatusCodes.Status404NotFound, 25 | ErrorType.InvalidData => StatusCodes.Status400BadRequest, 26 | _ => StatusCodes.Status500InternalServerError 27 | }; 28 | 29 | var failures = ToFailures(result.Errors); 30 | 31 | await ep.HttpContext.Response.SendErrorsAsync( 32 | failures, 33 | statusCode, 34 | cancellation: ct); 35 | 36 | static ErrorType GetErrorType(IError error) 37 | => (ErrorType)error.Metadata[nameof(ErrorType)]; 38 | 39 | static List ToFailures(List errors) 40 | => errors.Select(e => 41 | { 42 | var errorCode = e.Metadata[DomainError.ErrorCodeLiteral]; 43 | return new ValidationFailure(errorCode.ToString(), e.Message); 44 | }).ToList(); 45 | } 46 | } -------------------------------------------------------------------------------- /SuperNote.WebApi/Program.cs: -------------------------------------------------------------------------------- 1 | using FastEndpoints; 2 | using FastEndpoints.Swagger; 3 | using SuperNote.Application; 4 | using SuperNote.DataAccess; 5 | using SuperNote.Domain; 6 | using static SuperNote.WebApi.AssemblyReferences; 7 | 8 | var builder = WebApplication.CreateBuilder(args); 9 | 10 | builder.Services.AddFastEndpoints(); 11 | builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(WebApi, Application)); 12 | 13 | builder.Services.SwaggerDocument(o => 14 | { 15 | o.ShortSchemaNames = true; 16 | }); 17 | 18 | builder.Services.AddSingleton(TimeProvider.System); 19 | 20 | builder.Services 21 | .AddDomainServices() 22 | .AddApplicationServices() 23 | .AddDataAccessServices(builder.Configuration.GetValue("Sql:ConnectionString")); 24 | 25 | var app = builder.Build(); 26 | 27 | if (app.Environment.IsDevelopment()) 28 | { 29 | } 30 | 31 | app.UseFastEndpoints(x => x.Errors.UseProblemDetails()); 32 | 33 | app.UseSwaggerGen(); 34 | 35 | app.Run(); -------------------------------------------------------------------------------- /SuperNote.WebApi/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:58903", 8 | "sslPort": 44372 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5016", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7113;http://localhost:5016", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SuperNote.WebApi/SuperNote.WebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | a0819a06-721c-4f5a-a4f1-f90903e5343f 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SuperNote.WebApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "Sql": { 10 | "ConnectionString": "" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /super-note.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{073EC3EA-6DDD-43D7-8EF1-69B946DA06BF}" 4 | EndProject 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C46E42A8-6BE2-4640-BD41-72F287FB718C}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SuperNote.Domain", "SuperNote.Domain\SuperNote.Domain.csproj", "{64CAC520-F0F0-4A1C-86FF-9639B3BDAC30}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SuperNote.Application", "SuperNote.Application\SuperNote.Application.csproj", "{D12E00AF-BB29-4A66-B577-3A93A32E7CC2}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SuperNote.WebApi", "SuperNote.WebApi\SuperNote.WebApi.csproj", "{4E0292C8-4BE4-4637-964E-53A30E0264F3}" 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{183AC178-5566-418C-AECC-4773866FD7D6}" 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SuperNote.DataAccess", "SuperNote.DataAccess\SuperNote.DataAccess.csproj", "{FB8F5DA8-91BD-478B-AEF7-98A6E4331AF4}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SuperNote.Domain.Tests", "SuperNote.Domain.Tests\SuperNote.Domain.Tests.csproj", "{1BA12EC8-0B9C-4E7F-BE6E-ECA9B943413C}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SuperNote.Application.Tests", "SuperNote.Application.Tests\SuperNote.Application.Tests.csproj", "{FD9A16E4-F8F0-429D-9894-4ACD7E63E9CF}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(NestedProjects) = preSolution 27 | {64CAC520-F0F0-4A1C-86FF-9639B3BDAC30} = {073EC3EA-6DDD-43D7-8EF1-69B946DA06BF} 28 | {D12E00AF-BB29-4A66-B577-3A93A32E7CC2} = {073EC3EA-6DDD-43D7-8EF1-69B946DA06BF} 29 | {4E0292C8-4BE4-4637-964E-53A30E0264F3} = {073EC3EA-6DDD-43D7-8EF1-69B946DA06BF} 30 | {183AC178-5566-418C-AECC-4773866FD7D6} = {073EC3EA-6DDD-43D7-8EF1-69B946DA06BF} 31 | {FB8F5DA8-91BD-478B-AEF7-98A6E4331AF4} = {183AC178-5566-418C-AECC-4773866FD7D6} 32 | {1BA12EC8-0B9C-4E7F-BE6E-ECA9B943413C} = {C46E42A8-6BE2-4640-BD41-72F287FB718C} 33 | {FD9A16E4-F8F0-429D-9894-4ACD7E63E9CF} = {C46E42A8-6BE2-4640-BD41-72F287FB718C} 34 | EndGlobalSection 35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 36 | {64CAC520-F0F0-4A1C-86FF-9639B3BDAC30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {64CAC520-F0F0-4A1C-86FF-9639B3BDAC30}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {64CAC520-F0F0-4A1C-86FF-9639B3BDAC30}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {64CAC520-F0F0-4A1C-86FF-9639B3BDAC30}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {D12E00AF-BB29-4A66-B577-3A93A32E7CC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {D12E00AF-BB29-4A66-B577-3A93A32E7CC2}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {D12E00AF-BB29-4A66-B577-3A93A32E7CC2}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {D12E00AF-BB29-4A66-B577-3A93A32E7CC2}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {4E0292C8-4BE4-4637-964E-53A30E0264F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {4E0292C8-4BE4-4637-964E-53A30E0264F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {4E0292C8-4BE4-4637-964E-53A30E0264F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {4E0292C8-4BE4-4637-964E-53A30E0264F3}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {FB8F5DA8-91BD-478B-AEF7-98A6E4331AF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {FB8F5DA8-91BD-478B-AEF7-98A6E4331AF4}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {FB8F5DA8-91BD-478B-AEF7-98A6E4331AF4}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {FB8F5DA8-91BD-478B-AEF7-98A6E4331AF4}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {1BA12EC8-0B9C-4E7F-BE6E-ECA9B943413C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {1BA12EC8-0B9C-4E7F-BE6E-ECA9B943413C}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {1BA12EC8-0B9C-4E7F-BE6E-ECA9B943413C}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {1BA12EC8-0B9C-4E7F-BE6E-ECA9B943413C}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {FD9A16E4-F8F0-429D-9894-4ACD7E63E9CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {FD9A16E4-F8F0-429D-9894-4ACD7E63E9CF}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {FD9A16E4-F8F0-429D-9894-4ACD7E63E9CF}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {FD9A16E4-F8F0-429D-9894-4ACD7E63E9CF}.Release|Any CPU.Build.0 = Release|Any CPU 60 | EndGlobalSection 61 | EndGlobal 62 | --------------------------------------------------------------------------------