├── .dockerignore
├── .gitignore
├── AkilliPrompt.sln
├── LICENSE.md
├── README.md
├── logos
├── logo_240x202.png
└── yazilim_academy_logo_320.png
└── src
├── AkilliPrompt.Domain
├── AkilliPrompt.Domain.csproj
├── Common
│ ├── EntityBase.cs
│ ├── ICreatedByEntity.cs
│ ├── IDomainEvent.cs
│ └── IModifiedByEntity.cs
├── Constants
│ └── RoleConstants.cs
├── Entities
│ ├── Category.cs
│ ├── Placeholder.cs
│ ├── Prompt.cs
│ ├── PromptCategory.cs
│ ├── PromptComment.cs
│ ├── RefreshToken.cs
│ ├── UserFavoritePrompt.cs
│ ├── UserLikePrompt.cs
│ └── UserSocialMediaAccount.cs
├── Enums
│ └── SocialMediaType.cs
├── Helpers
│ └── IpHelper.cs
├── Identity
│ ├── ApplicationRole.cs
│ ├── ApplicationRoleClaim.cs
│ ├── ApplicationUser.cs
│ ├── ApplicationUserClaim.cs
│ ├── ApplicationUserLogin.cs
│ ├── ApplicationUserRole.cs
│ └── ApplicationUserToken.cs
├── Settings
│ ├── AzureKeyVaultSettings.cs
│ ├── CloudflareR2Settings.cs
│ ├── GoogleAuthSettings.cs
│ └── JwtSettings.cs
└── ValueObjects
│ ├── AccessToken.cs
│ ├── FullName.cs
│ ├── RefreshToken.cs
│ └── ValidationError.cs
├── AkilliPrompt.Persistence
├── AkilliPrompt.Persistence.csproj
├── DependencyInjection.cs
├── EntityFramework
│ ├── Configurations
│ │ ├── ApplicationRoleClaimConfiguration.cs
│ │ ├── ApplicationRoleConfiguration.cs
│ │ ├── ApplicationUserClaimConfiguration.cs
│ │ ├── ApplicationUserConfiguration.cs
│ │ ├── ApplicationUserLoginConfiguration.cs
│ │ ├── ApplicationUserRoleConfiguration.cs
│ │ ├── ApplicationUserTokenConfiguration.cs
│ │ ├── CategoryConfiguration.cs
│ │ ├── PlaceholderConfiguration.cs
│ │ ├── PromptCategoryConfiguration.cs
│ │ ├── PromptCommentConfiguration.cs
│ │ ├── PromptConfiguration.cs
│ │ ├── UserFavoritePromptConfiguration.cs
│ │ ├── UserLikePromptConfiguration.cs
│ │ └── UserSocialMediaAccountConfiguration.cs
│ ├── Contexts
│ │ ├── ApplicationDbContext.cs
│ │ └── ApplicationDbContextFactory.cs
│ ├── Extensions
│ │ └── ConvertionExtensions.cs
│ ├── Interceptors
│ │ └── EntityInterceptor.cs
│ ├── Migrations
│ │ ├── 20241201192259_InitialCreate.Designer.cs
│ │ ├── 20241201192259_InitialCreate.cs
│ │ └── ApplicationDbContextModelSnapshot.cs
│ └── Seeders
│ │ └── ApplicationRoleSeeder.cs
└── Services
│ └── ICurrentUserService.cs
└── AkilliPrompt.WebApi
├── AkilliPrompt.WebApi.csproj
├── Attributes
├── CacheKeyPartAttribute.cs
└── CacheOptionsAttribute.cs
├── Behaviors
├── CachingBehavior.cs
└── ValidationBehavior.cs
├── Common
└── FluentValidation
│ └── EntityExistsValidator.cs
├── Configuration
└── SwaggerConfiguration.cs
├── DependencyInjection.cs
├── Dockerfile
├── Extensions
└── ApplicationBuilderExtensions.cs
├── Filters
├── GlobalExceptionFilter.cs
└── SwaggerJsonIgnoreFilter.cs
├── Helpers
├── CacheKeysHelper.cs
└── MessageHelper.cs
├── Interfaces
├── ICacheable.cs
└── IPaginated.cs
├── Models
├── PaginatedList.cs
└── ResponseDto.cs
├── Options
└── ConfigureSwaggerOptions.cs
├── Program.cs
├── Properties
└── launchSettings.json
├── Services
├── CacheInvalidator.cs
├── CacheKeyFactory.cs
├── CurrentUserManager.cs
├── ExistenceManager.cs
├── IExistenceService.cs
├── JwtManager.cs
└── R2ObjectStorageManager.cs
├── V1
├── Auth
│ ├── AuthController.cs
│ └── Commands
│ │ └── GoogleLogin
│ │ ├── GoogleLoginCommand.cs
│ │ ├── GoogleLoginCommandHandler.cs
│ │ ├── GoogleLoginCommandValidator.cs
│ │ └── GoogleLoginDto.cs
├── Categories
│ ├── CategoriesController.cs
│ ├── Commands
│ │ ├── Create
│ │ │ ├── CreateCategoryCommand.cs
│ │ │ ├── CreateCategoryCommandHandler.cs
│ │ │ └── CreateCategoryCommandValidator.cs
│ │ ├── Delete
│ │ │ ├── DeleteCategoryCommand.cs
│ │ │ ├── DeleteCategoryCommandHandler.cs
│ │ │ └── DeleteCategoryValidator.cs
│ │ └── Update
│ │ │ ├── UpdateCategoryCommand.cs
│ │ │ ├── UpdateCategoryCommandHandler.cs
│ │ │ └── UpdateCategoryCommandValidator.cs
│ └── Queries
│ │ ├── GetAll
│ │ ├── GetAllCategoriesDto.cs
│ │ ├── GetAllCategoriesQuery.cs
│ │ └── GetAllCategoriesQueryHandler.cs
│ │ └── GetById
│ │ ├── GetByIdCategoryDto.cs
│ │ ├── GetByIdCategoryQuery.cs
│ │ ├── GetByIdCategoryQueryHandler.cs
│ │ └── GetByIdCategoryQueryValidator.cs
├── PromptComments
│ ├── PromptCommentsController.cs
│ └── Queries
│ │ └── GetAll
│ │ ├── GetAllPromptCommentsDto.cs
│ │ ├── GetAllPromptCommentsQuery.cs
│ │ ├── GetAllPromptCommentsQueryHandler.cs
│ │ └── GetAllPromptCommentsQueryValidator.cs
└── Prompts
│ ├── Commands
│ ├── Create
│ │ ├── CreatePromptCommand.cs
│ │ ├── CreatePromptCommandHandler.cs
│ │ └── CreatePromptCommandValidator.cs
│ └── Delete
│ │ ├── DeletePromptCommand.cs
│ │ ├── DeletePromptCommandHandler.cs
│ │ └── DeletePromptCommandValidator.cs
│ ├── PromptsController.cs
│ ├── Queries
│ ├── GetAll
│ │ ├── GetAllPromptsDto.cs
│ │ ├── GetAllPromptsQuery.cs
│ │ ├── GetAllPromptsQueryHandler.cs
│ │ └── GetAllPromptsQueryValidator.cs
│ └── GetById
│ │ ├── GetPromptByIdDto.cs
│ │ ├── GetPromptByIdQuery.cs
│ │ ├── GetPromptByIdQueryHandler.cs
│ │ └── GetPromptByIdQueryValidator.cs
│ └── UpdatePromptDto.cs
├── appsettings.Development.json
└── appsettings.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.classpath
2 | **/.dockerignore
3 | **/.env
4 | **/.git
5 | **/.gitignore
6 | **/.project
7 | **/.settings
8 | **/.toolstarget
9 | **/.vs
10 | **/.vscode
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/azds.yaml
15 | **/bin
16 | **/charts
17 | **/docker-compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | LICENSE
25 | README.md
26 | !**/.gitignore
27 | !.git/HEAD
28 | !.git/config
29 | !.git/packed-refs
30 | !.git/refs/heads/**
--------------------------------------------------------------------------------
/.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 |
33 | # Visual Studio 2015/2017 cache/options directory
34 | .vs/
35 | # Uncomment if you have tasks that create the project's static files in wwwroot
36 | #wwwroot/
37 |
38 | # Visual Studio 2017 auto generated files
39 | Generated\ Files/
40 |
41 | # MSTest test Results
42 | [Tt]est[Rr]esult*/
43 | [Bb]uild[Ll]og.*
44 |
45 | # NUnit
46 | *.VisualState.xml
47 | TestResult.xml
48 | nunit-*.xml
49 |
50 | # Build Results of an ATL Project
51 | [Dd]ebugPS/
52 | [Rr]eleasePS/
53 | dlldata.c
54 |
55 | # Benchmark Results
56 | BenchmarkDotNet.Artifacts/
57 |
58 | # .NET Core
59 | project.lock.json
60 | project.fragment.lock.json
61 | artifacts/
62 |
63 | # StyleCop
64 | StyleCopReport.xml
65 |
66 | # Files built by Visual Studio
67 | *_i.c
68 | *_p.c
69 | *_h.h
70 | *.ilk
71 | *.meta
72 | *.obj
73 | *.iobj
74 | *.pch
75 | *.pdb
76 | *.ipdb
77 | *.pgc
78 | *.pgd
79 | *.rsp
80 | *.sbr
81 | *.tlb
82 | *.tli
83 | *.tlh
84 | *.tmp
85 | *.tmp_proj
86 | *_wpftmp.csproj
87 | *.log
88 | *.vspscc
89 | *.vssscc
90 | .builds
91 | *.pidb
92 | *.svclog
93 | *.scc
94 |
95 | # Chutzpah Test files
96 | _Chutzpah*
97 |
98 | # Visual C++ cache files
99 | ipch/
100 | *.aps
101 | *.ncb
102 | *.opendb
103 | *.opensdf
104 | *.sdf
105 | *.cachefile
106 | *.VC.db
107 | *.VC.VC.opendb
108 |
109 | # Visual Studio profiler
110 | *.psess
111 | *.vsp
112 | *.vspx
113 | *.sap
114 |
115 | # Visual Studio Trace Files
116 | *.e2e
117 |
118 | # TFS 2012 Local Workspace
119 | $tf/
120 |
121 | # Guidance Automation Toolkit
122 | *.gpState
123 |
124 | # ReSharper is a .NET coding add-in
125 | _ReSharper*/
126 | *.[Rr]e[Ss]harper
127 | *.DotSettings.user
128 |
129 | # JustCode is a .NET coding add-in
130 | .JustCode
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 | src/AkilliPrompt.WebApi/Logs/
352 |
--------------------------------------------------------------------------------
/AkilliPrompt.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.12.35514.174 d17.12
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3BB6ED29-C82B-4633-A40D-7FDB570168DE}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{17C870BC-239B-4DEA-94C7-6D9DE7887C3A}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AkilliPrompt.Domain", "src\AkilliPrompt.Domain\AkilliPrompt.Domain.csproj", "{C8488BCA-BC9F-484A-9450-4DF494B2E6C7}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AkilliPrompt.Persistence", "src\AkilliPrompt.Persistence\AkilliPrompt.Persistence.csproj", "{3400E90E-7504-40DD-A242-A3B291B3DB05}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AkilliPrompt.WebApi", "src\AkilliPrompt.WebApi\AkilliPrompt.WebApi.csproj", "{2A6044C5-6A4C-4E38-B5D8-2581523B017A}"
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Release|Any CPU = Release|Any CPU
20 | EndGlobalSection
21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
22 | {C8488BCA-BC9F-484A-9450-4DF494B2E6C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {C8488BCA-BC9F-484A-9450-4DF494B2E6C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {C8488BCA-BC9F-484A-9450-4DF494B2E6C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {C8488BCA-BC9F-484A-9450-4DF494B2E6C7}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {3400E90E-7504-40DD-A242-A3B291B3DB05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {3400E90E-7504-40DD-A242-A3B291B3DB05}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {3400E90E-7504-40DD-A242-A3B291B3DB05}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {3400E90E-7504-40DD-A242-A3B291B3DB05}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {2A6044C5-6A4C-4E38-B5D8-2581523B017A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {2A6044C5-6A4C-4E38-B5D8-2581523B017A}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {2A6044C5-6A4C-4E38-B5D8-2581523B017A}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {2A6044C5-6A4C-4E38-B5D8-2581523B017A}.Release|Any CPU.Build.0 = Release|Any CPU
34 | EndGlobalSection
35 | GlobalSection(SolutionProperties) = preSolution
36 | HideSolutionNode = FALSE
37 | EndGlobalSection
38 | GlobalSection(NestedProjects) = preSolution
39 | {C8488BCA-BC9F-484A-9450-4DF494B2E6C7} = {3BB6ED29-C82B-4633-A40D-7FDB570168DE}
40 | {3400E90E-7504-40DD-A242-A3B291B3DB05} = {3BB6ED29-C82B-4633-A40D-7FDB570168DE}
41 | {2A6044C5-6A4C-4E38-B5D8-2581523B017A} = {3BB6ED29-C82B-4633-A40D-7FDB570168DE}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Yazılım Academy
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 | # Akıllı Prompt Backend
2 |
3 |
4 |

5 |
6 |

7 |
8 |
9 |
10 | Merhaba, Türkiye'nin geliştirici topluluğu! Şu an üzerinde çalıştığımız "Akıllı Prompt" projesine hoşgeldiniz. Bu proje, ChatGPT, Claude, Gemini, MidJourney gibi farklı yapay zeka modellerinin kullanıcıları için bir "prompt" kütüphanesi ve topluluğu yaratmayı amaçlıyor. Bizim amacımız, bu alandaki geliştiricileri ve meraklıları bir araya getirerek, paylaşım ve dayanışma ruhunu canlı tutmak. Projemizi açık kaynak hale getirerek Türkiye geliştirici topluluğuna katkıda bulunmak istiyoruz. 🤝
11 |
12 | ## Proje Hakkında
13 |
14 | Bu proje, Minimum Viable Product (MVP) anlayışıyla geliştirilmekte olup, öğren, inşa et ve tekrar et döngüsüyle ilerlemeyi hedefliyoruz. Kullanıcı geri bildirimlerine dayalı olarak sürekli iyileştirme ve büyüme odaklı bir yaklaşımla hareket ediyoruz.
15 |
16 | Akıllı Prompt, kullanıcıların farklı yapay zeka modelleriyle kullanabileceği etkili "prompt"lar oluşturabileceği bir SaaS uygulamasıdır. Bu uygulama, hem ücretli bir hizmet sunacak hem de arka uç ve ön uç tarafından açık kaynak kodu sunarak geliştiricilere açık olacak. Bu repo, projenin .NET 9 ve ASP.NET Core Web API kullanılarak geliştirilmiş arka uç (backend) bileşenini içermektedir.
17 |
18 | ## Mimari
19 |
20 | Proje "Clean Architecture" prensiplerine uygun olarak tasarlanmıştır ve aşağıdaki katmanlardan oluşmaktadır:
21 |
22 | - **Domain**: Uygulamanın temel iş mantığını ve kurallarını barındıran katmandır.
23 | - **Persistence**: Veri erişimi ve veritabanı işlemlerini yürüten katmandır. Entity Framework Core kullanılmaktadır.
24 | - **Web API**: RESTful API'leri barındıran ve kullanıcı taleplerine cevap veren katmandır.
25 |
26 | ## Kurulum
27 |
28 | Projeyi kendi bilgisayarınızda çalıştırmak için aşağıdaki adımları izleyebilirsiniz:
29 |
30 | 1. **Depoyu Klonlayın**
31 |
32 | ```sh
33 | git clone https://github.com/YazilimAcademy/AkilliPrompt.git
34 | cd AkilliPrompt
35 | ```
36 |
37 | 2. **Bağımlılıkları Yükleyin**
38 |
39 | - Proje .NET 9 kullanıyor, bu nedenle .NET 9 SDK'sını kurduğunuzdan emin olun.
40 | - Entity Framework Core, proje bağımlılıklarından biridir. Aşağıdaki komutu kullanarak veritabanı güncellemelerini uygulayabilirsiniz:
41 | ```sh
42 | dotnet ef database update
43 | ```
44 |
45 | 3. **Uygulamayı Çalıştırın**
46 |
47 | ```sh
48 | dotnet run --project AkilliPrompt.WebApi
49 | ```
50 |
51 | ## Katkıda Bulunma
52 |
53 | Bu projenin gelişiminde katkıda bulunmak isteyen herkesi bekliyoruz! Katkıda bulunmak için:
54 |
55 | 1. Bir "issue" oluşturarak hataları bildirin veya önerilerde bulunun.
56 | 2. Yeni bir özellik eklemek ya da hata düzeltmek istiyorsanız, bir "pull request" oluşturabilirsiniz.
57 | 3. Katkı sağlarken, lütfen "Clean Code" prensiplerine ve projenin kod standartlarına uygun şekilde kod yazmaya dikkat edin.
58 |
59 | ## Lisans
60 |
61 | Bu proje, MIT Lisansı altında sunulmuştur. Daha fazla bilgi için `LICENSE` dosyasına göz atabilirsiniz.
62 |
63 | Hep birlikte Türkiye'deki yazılım geliştirici topluluğunu daha da ileri taşıyalım!
64 |
65 | ## Topluluk ve İletişim
66 |
67 | Akıllı Prompt projesinin geliştirilmesi ve büyütülmesi sürecinde siz de yer almak ister misiniz? Gelin, birlikte öğrenip gelişelim! 🤝
68 |
69 | - **Discord**: Topluluğumuza katılın ve diğer geliştiricilerle sohbet edin. [Discord Bağlantısı](https://discord.gg/yazilimacademy)
70 | - **Yazılım Academy Web**: Daha fazla bilgi ve kaynak için [Yazılım Academy Web Sitesi](https://yazilim.academy/)
71 | - **YouTube**: Eğitim videoları ve duyurular için [YouTube Kanalımız](https://www.youtube.com/@yazilimacademy)
72 |
73 |
74 |
75 |
76 | ### Oricin ve Yazılım Academy’deki ekip arkadaşlarımıza çok teşekkür ederiz. 👇
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | ## Teşekkürler
92 | Projemize gösterdiğiniz ilgi ve desteğiniz için çok teşekkür ederiz. Birlikte daha büyük bir topluluk oluşturabilir ve daha faydalı projeler geliştirebiliriz. 🙏
93 |
--------------------------------------------------------------------------------
/logos/logo_240x202.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yazilimacademy/akilliprompt-backend/d0d8c7f7017d0677ff5c41f03516177f38f33f2a/logos/logo_240x202.png
--------------------------------------------------------------------------------
/logos/yazilim_academy_logo_320.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yazilimacademy/akilliprompt-backend/d0d8c7f7017d0677ff5c41f03516177f38f33f2a/logos/yazilim_academy_logo_320.png
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/AkilliPrompt.Domain.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Common/EntityBase.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.Common;
2 |
3 | public abstract class EntityBase : ICreatedByEntity, IModifiedByEntity
4 | {
5 | public Guid Id { get; set; }
6 |
7 | public string? CreatedByUserId { get; set; }
8 | public DateTimeOffset CreatedAt { get; set; }
9 |
10 | public string? ModifiedByUserId { get; set; }
11 | public DateTimeOffset? ModifiedAt { get; set; }
12 |
13 | private readonly List _domainEvents = [];
14 | public IReadOnlyList DomainEvents => _domainEvents.AsReadOnly();
15 |
16 | protected void AddDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent);
17 | protected void ClearDomainEvents() => _domainEvents.Clear();
18 | }
19 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Common/ICreatedByEntity.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.Common;
2 |
3 | public interface ICreatedByEntity
4 | {
5 | string? CreatedByUserId { get; set; }
6 | DateTimeOffset CreatedAt { get; set; }
7 | }
8 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Common/IDomainEvent.cs:
--------------------------------------------------------------------------------
1 | using MediatR;
2 |
3 | namespace AkilliPrompt.Domain.Common;
4 |
5 | public interface IDomainEvent : INotification
6 | {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Common/IModifiedByEntity.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.Common;
2 |
3 | public interface IModifiedByEntity
4 | {
5 | string? ModifiedByUserId { get; set; }
6 | DateTimeOffset? ModifiedAt { get; set; }
7 | }
8 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Constants/RoleConstants.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.Constants;
2 |
3 | public static class RoleConstants
4 | {
5 | public const string UserRole = "User";
6 | public const string AdminRole = "Admin";
7 | }
8 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Entities/Category.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Common;
2 | using TSID.Creator.NET;
3 |
4 | namespace AkilliPrompt.Domain.Entities;
5 |
6 | public sealed class Category : EntityBase
7 | {
8 | public string Name { get; private set; }
9 | public string Description { get; private set; }
10 |
11 | public ICollection PromptCategories { get; set; } = [];
12 |
13 | public static Category Create(string name, string description)
14 | {
15 | return new Category
16 | {
17 | Id = Guid.CreateVersion7(),
18 | Name = name,
19 | Description = description,
20 | };
21 | }
22 |
23 | public void Update(string name, string description)
24 | {
25 | Name = name;
26 | Description = description;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Entities/Placeholder.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Common;
2 | using TSID.Creator.NET;
3 |
4 | namespace AkilliPrompt.Domain.Entities;
5 |
6 | public sealed class Placeholder : EntityBase
7 | {
8 | public string Name { get; set; }
9 | public Guid PromptId { get; set; }
10 | public Prompt Prompt { get; set; }
11 |
12 | public static Placeholder Create(string name, Guid promptId)
13 | {
14 | return new Placeholder
15 | {
16 | Id = Guid.CreateVersion7(),
17 | Name = name,
18 | PromptId = promptId
19 | };
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Entities/Prompt.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Common;
2 | using AkilliPrompt.Domain.Identity;
3 |
4 | namespace AkilliPrompt.Domain.Entities;
5 |
6 | public sealed class Prompt : EntityBase
7 | {
8 | public string Title { get; private set; }
9 | public string Description { get; private set; }
10 | public string Content { get; private set; }
11 |
12 | public string? ImageUrl { get; private set; }
13 | public bool IsActive { get; private set; }
14 | public int LikeCount { get; private set; }
15 |
16 | public Guid CreatorId { get; private set; }
17 | public ApplicationUser Creator { get; private set; }
18 |
19 | public ICollection PromptCategories { get; private set; } = [];
20 | public ICollection UserFavoritePrompts { get; set; } = [];
21 | public ICollection UserLikePrompts { get; set; } = [];
22 | public ICollection Placeholders { get; set; } = [];
23 | public ICollection PromptComments { get; set; } = [];
24 |
25 |
26 | public static Prompt Create(string title, string description, string content, bool isActive, Guid creatorId)
27 | {
28 | return new Prompt
29 | {
30 | Id = Guid.CreateVersion7(),
31 | Title = title,
32 | Description = description,
33 | Content = content,
34 | IsActive = isActive,
35 | CreatorId = creatorId
36 | };
37 | }
38 |
39 | public void SetImageUrl(string imageUrl)
40 | {
41 | ImageUrl = imageUrl;
42 | }
43 |
44 | public void Update(string title, string description, string content, bool isActive)
45 | {
46 | Title = title;
47 | Description = description;
48 | Content = content;
49 | IsActive = isActive;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Entities/PromptCategory.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Common;
2 | using TSID.Creator.NET;
3 |
4 | namespace AkilliPrompt.Domain.Entities;
5 |
6 | public sealed class PromptCategory : EntityBase
7 | {
8 | public Guid PromptId { get; set; }
9 | public Prompt Prompt { get; set; }
10 |
11 | public Guid CategoryId { get; set; }
12 | public Category Category { get; set; }
13 |
14 | public static PromptCategory Create(Guid promptId, Guid categoryId)
15 | {
16 | return new PromptCategory
17 | {
18 | Id = Guid.CreateVersion7(),
19 | PromptId = promptId,
20 | CategoryId = categoryId,
21 | };
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Entities/PromptComment.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Common;
2 | using AkilliPrompt.Domain.Identity;
3 |
4 | namespace AkilliPrompt.Domain.Entities;
5 |
6 | public sealed class PromptComment : EntityBase
7 | {
8 | public int Level { get; set; }
9 | public string Content { get; set; }
10 |
11 | public Guid PromptId { get; set; }
12 | public Prompt Prompt { get; set; }
13 |
14 | public Guid UserId { get; set; }
15 | public ApplicationUser User { get; set; }
16 |
17 | public Guid? ParentCommentId { get; set; }
18 | public PromptComment ParentComment { get; set; }
19 |
20 | public ICollection ChildComments { get; set; } = [];
21 | }
22 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Entities/RefreshToken.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Common;
2 | using AkilliPrompt.Domain.Identity;
3 |
4 | namespace AkilliPrompt.Domain.Entities;
5 | public class RefreshToken : EntityBase
6 | {
7 | public string Token { get; set; }
8 | public DateTime Expires { get; set; }
9 | public string CreatedByIp { get; set; }
10 | public string SecurityStamp { get; set; }
11 | public DateTime? Revoked { get; set; }
12 | public string? RevokedByIp { get; set; }
13 | public Guid UserId { get; set; }
14 | public virtual ApplicationUser User { get; set; }
15 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Entities/UserFavoritePrompt.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Common;
2 | using AkilliPrompt.Domain.Identity;
3 |
4 | namespace AkilliPrompt.Domain.Entities;
5 |
6 | public sealed class UserFavoritePrompt : EntityBase
7 | {
8 | public Guid UserId { get; set; }
9 | public ApplicationUser User { get; set; }
10 |
11 | public Guid PromptId { get; set; }
12 | public Prompt Prompt { get; set; }
13 | }
14 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Entities/UserLikePrompt.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Common;
2 | using AkilliPrompt.Domain.Identity;
3 |
4 | namespace AkilliPrompt.Domain.Entities;
5 |
6 | public sealed class UserLikePrompt : EntityBase
7 | {
8 | public Guid UserId { get; set; }
9 | public ApplicationUser User { get; set; }
10 |
11 | public Guid PromptId { get; set; }
12 | public Prompt Prompt { get; set; }
13 | }
14 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Entities/UserSocialMediaAccount.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Common;
2 | using AkilliPrompt.Domain.Enums;
3 | using AkilliPrompt.Domain.Identity;
4 |
5 | namespace AkilliPrompt.Domain.Entities;
6 |
7 | public sealed class UserSocialMediaAccount : EntityBase
8 | {
9 | public SocialMediaType SocialMediaType { get; set; }
10 | public string Url { get; set; }
11 |
12 | public Guid UserId { get; set; }
13 | public ApplicationUser User { get; set; }
14 | }
15 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Enums/SocialMediaType.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.Enums;
2 |
3 | public enum SocialMediaType
4 | {
5 | LinkedIn = 1,
6 | X = 2,
7 | GitHub = 3,
8 | Instagram = 4,
9 | Facebook = 5,
10 | TikTok = 6,
11 | YouTube = 7,
12 | Reddit = 8,
13 | Medium = 9,
14 | Website = 10,
15 | }
16 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Helpers/IpHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Sockets;
3 |
4 | namespace AkilliPrompt.Domain.Helpers;
5 |
6 | public static class IpHelper
7 | {
8 | public static string GetIpAddress()
9 | {
10 | var host = Dns.GetHostEntry(Dns.GetHostName());
11 |
12 | foreach (var ip in host.AddressList)
13 | {
14 | if (ip.AddressFamily == AddressFamily.InterNetwork)
15 | {
16 | return ip.ToString();
17 | }
18 | }
19 | return string.Empty;
20 | }
21 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Identity/ApplicationRole.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Identity;
2 |
3 | namespace AkilliPrompt.Domain.Identity;
4 |
5 | public sealed class ApplicationRole : IdentityRole
6 | {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Identity/ApplicationRoleClaim.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Identity;
2 |
3 | namespace AkilliPrompt.Domain.Identity;
4 |
5 | public sealed class ApplicationRoleClaim : IdentityRoleClaim
6 | {
7 | }
8 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Identity/ApplicationUser.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Common;
2 | using AkilliPrompt.Domain.Entities;
3 | using AkilliPrompt.Domain.ValueObjects;
4 | using Microsoft.AspNetCore.Identity;
5 |
6 | namespace AkilliPrompt.Domain.Identity;
7 |
8 | public sealed class ApplicationUser : IdentityUser, ICreatedByEntity, IModifiedByEntity
9 | {
10 | public FullName FullName { get; set; }
11 |
12 | public DateTimeOffset CreatedAt { get; set; }
13 | public string? CreatedByUserId { get; set; }
14 |
15 | public string? ModifiedByUserId { get; set; }
16 | public DateTimeOffset? ModifiedAt { get; set; }
17 |
18 | public ICollection UserSocialMediaAccounts { get; set; } = [];
19 | public ICollection PromptComments { get; set; } = [];
20 | public ICollection UserFavoritePrompts { get; set; } = [];
21 | public ICollection UserLikePrompts { get; set; } = [];
22 | public ICollection CreatedPrompts { get; set; } = [];
23 |
24 | public static ApplicationUser Create(string email, FullName fullName, bool isEmailConfirmed = false)
25 | {
26 | return new ApplicationUser
27 | {
28 | Id = Guid.CreateVersion7(),
29 | Email = email,
30 | UserName = email,
31 | FullName = fullName,
32 | EmailConfirmed = isEmailConfirmed,
33 | SecurityStamp = Guid.NewGuid().ToString()
34 | };
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Identity/ApplicationUserClaim.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Identity;
2 |
3 | namespace AkilliPrompt.Domain.Identity;
4 |
5 | public sealed class ApplicationUserClaim : IdentityUserClaim
6 | {
7 | }
8 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Identity/ApplicationUserLogin.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Identity;
2 |
3 | namespace AkilliPrompt.Domain.Identity;
4 |
5 | public sealed class ApplicationUserLogin : IdentityUserLogin
6 | {
7 | }
8 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Identity/ApplicationUserRole.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Identity;
2 |
3 | namespace AkilliPrompt.Domain.Identity;
4 |
5 | public sealed class ApplicationUserRole : IdentityUserRole
6 | {
7 | }
8 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Identity/ApplicationUserToken.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Identity;
2 |
3 | namespace AkilliPrompt.Domain.Identity;
4 |
5 | public sealed class ApplicationUserToken : IdentityUserToken
6 | {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Settings/AzureKeyVaultSettings.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.Settings;
2 |
3 | public sealed record AzureKeyVaultSettings
4 | {
5 | public string Uri { get; init; }
6 | public string TenantId { get; init; }
7 | public string ClientId { get; init; }
8 | public string ClientSecret { get; init; }
9 | }
10 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Settings/CloudflareR2Settings.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.Settings;
2 |
3 | public sealed record CloudflareR2Settings
4 | {
5 | public string ServiceUrl { get; set; }
6 | public string AccessKey { get; set; }
7 | public string SecretKey { get; set; }
8 | public string PromptPicsBucketName { get; set; }
9 | public string UserPicsBucketName { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Settings/GoogleAuthSettings.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.Settings;
2 |
3 | public sealed record GoogleAuthSettings
4 | {
5 | public string ClientId { get; init; }
6 |
7 | public GoogleAuthSettings(string clientId)
8 | {
9 | ClientId = clientId;
10 | }
11 |
12 | public GoogleAuthSettings()
13 | {
14 |
15 | }
16 | };
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/Settings/JwtSettings.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.Settings;
2 |
3 | public record JwtSettings
4 | {
5 | public string SecretKey { get; set; }
6 | public TimeSpan AccessTokenExpiration { get; set; }
7 | public TimeSpan RefreshTokenExpiration { get; set; }
8 | public string Issuer { get; set; }
9 | public string Audience { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/ValueObjects/AccessToken.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.ValueObjects;
2 |
3 | public sealed record AccessToken
4 | {
5 | public string Value { get; set; }
6 | public DateTime ExpiresOnUtc { get; set; }
7 | public bool IsExpired() => ExpiresOnUtc < DateTime.UtcNow;
8 |
9 | public AccessToken(string value, DateTime expiresOnUtc)
10 | {
11 | Value = value;
12 | ExpiresOnUtc = expiresOnUtc;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/ValueObjects/FullName.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace AkilliPrompt.Domain.ValueObjects
5 | {
6 | ///
7 | /// Represents a user's full name as a value object.
8 | ///
9 | public sealed record FullName
10 | {
11 | private const string NamePattern = @"^[\p{L}][\p{L}'\- ]+$"; // Supports Unicode letters, apostrophes, hyphens, and spaces
12 | private const int MinNameLength = 2;
13 | private const int MaxNameLength = 50; // Adjusted per name part
14 |
15 | ///
16 | /// Gets the first name.
17 | ///
18 | public string FirstName { get; init; }
19 |
20 | ///
21 | /// Gets the last name.
22 | ///
23 | public string LastName { get; init; }
24 |
25 | ///
26 | /// Initializes a new instance of the record.
27 | ///
28 | /// The first name.
29 | /// The last name.
30 | /// Thrown when validation fails.
31 | public FullName(string firstName, string lastName)
32 | {
33 | FirstName = ValidateName(firstName, nameof(firstName));
34 | LastName = ValidateName(lastName, nameof(lastName));
35 | }
36 |
37 | ///
38 | /// Validates the name according to defined rules.
39 | ///
40 | /// The name to validate.
41 | /// The parameter name for exception messages.
42 | /// The validated name.
43 | /// Thrown when validation fails.
44 | private static string ValidateName(string name, string paramName)
45 | {
46 | if (string.IsNullOrWhiteSpace(name))
47 | throw new ArgumentException($"{paramName} cannot be null or whitespace.", paramName);
48 |
49 | if (name.Length < MinNameLength || name.Length > MaxNameLength)
50 | throw new ArgumentException($"{paramName} must be between {MinNameLength} and {MaxNameLength} characters.", paramName);
51 |
52 | if (!Regex.IsMatch(name, NamePattern))
53 | throw new ArgumentException($"{paramName} contains invalid characters.", paramName);
54 |
55 | // Optional: Capitalize the first letter and lowercase the rest for consistency
56 | return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(name.ToLower());
57 | }
58 |
59 | ///
60 | /// Creates a from a single string containing the full name.
61 | /// Assumes the last word is the last name and the rest constitute the first name.
62 | ///
63 | /// The full name string.
64 | /// A new instance of .
65 | /// Thrown when validation fails.
66 | public static FullName Create(string fullName)
67 | {
68 | if (string.IsNullOrWhiteSpace(fullName))
69 | throw new ArgumentException("Full name cannot be null or whitespace.", nameof(fullName));
70 |
71 | var parts = fullName.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
72 |
73 | if (parts.Length < 2)
74 | throw new ArgumentException("Full name must contain at least first name and last name.", nameof(fullName));
75 |
76 | string firstName = string.Join(' ', parts.Take(parts.Length - 1));
77 | string lastName = parts.Last();
78 |
79 | return new FullName(firstName, lastName);
80 | }
81 |
82 | ///
83 | /// Creates a from separate first and last name strings.
84 | ///
85 | /// The first name.
86 | /// The last name.
87 | /// A new instance of .
88 | public static FullName Create(string firstName, string lastName)
89 | {
90 | return new FullName(firstName, lastName);
91 | }
92 |
93 | ///
94 | /// Explicitly converts a to a string.
95 | ///
96 | /// The full name to convert.
97 | public static explicit operator string(FullName fullName) => fullName.ToString();
98 |
99 | ///
100 | /// Explicitly converts a string to a .
101 | ///
102 | /// The string to convert.
103 | public static explicit operator FullName(string value) => Create(value);
104 |
105 | ///
106 | /// Returns the full name as a single string.
107 | ///
108 | /// The full name.
109 | public override string ToString() => $"{FirstName} {LastName}";
110 |
111 | ///
112 | /// Gets the initials of the full name.
113 | ///
114 | /// The initials in the format "F.L.".
115 | public string GetInitials()
116 | {
117 | char firstInitial = FirstName.FirstOrDefault(char.IsLetter);
118 | char lastInitial = LastName.FirstOrDefault(char.IsLetter);
119 |
120 | return $"{char.ToUpper(firstInitial)}.{char.ToUpper(lastInitial)}.";
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/ValueObjects/RefreshToken.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.ValueObjects;
2 |
3 | public sealed record RefreshToken
4 | {
5 | public string Value { get; set; }
6 | public DateTime ExpiresOnUtc { get; set; }
7 |
8 | public bool IsExpired() => ExpiresOnUtc < DateTime.UtcNow;
9 |
10 | public RefreshToken(string value, DateTime expiresOnUtc)
11 | {
12 | Value = value;
13 | ExpiresOnUtc = expiresOnUtc;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Domain/ValueObjects/ValidationError.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.Domain.ValueObjects;
2 |
3 | public sealed record ValidationError
4 | {
5 | public string PropertyName { get; init; }
6 | public IEnumerable ErrorMessages { get; init; }
7 |
8 | public ValidationError(string propertyName, IEnumerable errorMessages)
9 | {
10 | PropertyName = propertyName;
11 | ErrorMessages = errorMessages;
12 | }
13 |
14 | public ValidationError(string propertyName, string errorMessage)
15 | : this(propertyName, new List { errorMessage })
16 | {
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/AkilliPrompt.Persistence.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 | all
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/DependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AkilliPrompt.Persistence.EntityFramework.Contexts;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Diagnostics;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.DependencyInjection;
7 |
8 | namespace AkilliPrompt.Persistence;
9 |
10 | public static class DependencyInjection
11 | {
12 | public static IServiceCollection AddPersistence(this IServiceCollection services, IConfiguration configuration)
13 | {
14 | var connectionString = configuration.GetConnectionString("DefaultConnection");
15 |
16 | services.AddDbContext(options => options.UseNpgsql(connectionString, b => b.MigrationsHistoryTable("__ef_migrations_history")).ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.PendingModelChangesWarning)));
17 |
18 | return services;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/ApplicationRoleClaimConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Identity;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace IAPriceTrackerApp.Persistence.Configurations;
6 |
7 | public sealed class ApplicationRoleClaimConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Primary key
12 | builder.HasKey(rc => rc.Id);
13 |
14 | // Maps to the AspNetRoleClaims table
15 | builder.ToTable("application_role_claims");
16 | }
17 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/ApplicationRoleConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Identity;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.Configurations;
6 |
7 | public sealed class ApplicationRoleConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Primary key
12 | builder.HasKey(r => r.Id);
13 |
14 | // Index for "normalized" role name to allow efficient lookups
15 | builder.HasIndex(r => r.NormalizedName).HasDatabaseName("RoleNameIndex").IsUnique();
16 |
17 | // Maps to the AspNetRoles table
18 | // builder.ToTable("Roles");
19 |
20 | // A concurrency token for use with the optimistic concurrency checking
21 | builder.Property(r => r.ConcurrencyStamp).IsConcurrencyToken();
22 |
23 | // Limit the size of columns to use efficient database types
24 | builder.Property(u => u.Name).HasMaxLength(100);
25 | builder.Property(u => u.NormalizedName).HasMaxLength(100);
26 |
27 | // The relationships between Role and other entity types
28 | // Note that these relationships are configured with no navigation properties
29 |
30 | // Each Role can have many entries in the UserRole join table
31 | builder.HasMany().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();
32 |
33 | // Each Role can have many associated RoleClaims
34 | builder.HasMany().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
35 |
36 | builder.ToTable("application_roles");
37 | }
38 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/ApplicationUserClaimConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Identity;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.Configurations;
6 |
7 | public sealed class ApplicationUserClaimConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Primary key
12 | builder.HasKey(rc => rc.Id);
13 |
14 | // Maps to the AspNetUserClaims table
15 | builder.ToTable("application_user_claims");
16 | }
17 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/ApplicationUserConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Identity;
2 | using AkilliPrompt.Domain.ValueObjects;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
5 |
6 | namespace AkilliPrompt.Persistence.Configurations;
7 |
8 | public sealed class ApplicationUserConfiguration : IEntityTypeConfiguration
9 | {
10 | public void Configure(EntityTypeBuilder builder)
11 | {
12 | //Id
13 | builder.HasKey(x => x.Id);
14 |
15 | // Indexes for "normalized" username and email, to allow efficient lookups
16 | builder.HasIndex(u => u.NormalizedUserName).HasDatabaseName("UserNameIndex").IsUnique();
17 | builder.HasIndex(u => u.NormalizedEmail).HasDatabaseName("EmailIndex");
18 |
19 | // A concurrency token for use with the optimistic concurrency checking
20 | builder.Property(u => u.ConcurrencyStamp).IsConcurrencyToken();
21 |
22 | // Limit the size of columns to use efficient database types
23 | builder.Property(u => u.UserName).HasMaxLength(100);
24 | builder.Property(u => u.NormalizedUserName).HasMaxLength(100);
25 |
26 | //Email
27 | builder.Property(u => u.Email).IsRequired();
28 | builder.HasIndex(user => user.Email).IsUnique();
29 | builder.Property(u => u.Email).HasMaxLength(150);
30 | builder.Property(u => u.NormalizedEmail).HasMaxLength(150);
31 |
32 | //PhoneNumber
33 | builder.Property(u => u.PhoneNumber).IsRequired(false);
34 | builder.Property(u => u.PhoneNumber).HasMaxLength(20);
35 |
36 | // //FullName
37 | // builder.OwnsOne(x => x.FullName, fullName =>
38 | // {
39 | // fullName.Property(x => x.FirstName)
40 | // .IsRequired()
41 | // .HasMaxLength(50)
42 | // .HasColumnName("first_name");
43 |
44 | // fullName.Property(x => x.LastName)
45 | // .IsRequired()
46 | // .HasMaxLength(50)
47 | // .HasColumnName("last_name");
48 |
49 | // });
50 |
51 | builder.Property(x => x.FullName)
52 | .HasConversion(x => x.ToString(), x => FullName.Create(x));
53 |
54 |
55 | // The relationships between User and other entity types
56 | // Note that these relationships are configured with no navigation properties
57 |
58 | // Each User can have many UserClaims
59 | builder.HasMany().WithOne().HasForeignKey(uc => uc.UserId).IsRequired();
60 |
61 | // Each User can have many UserLogins
62 | builder.HasMany().WithOne().HasForeignKey(ul => ul.UserId).IsRequired();
63 |
64 | // Each User can have many UserTokens
65 | builder.HasMany().WithOne().HasForeignKey(ut => ut.UserId).IsRequired();
66 |
67 | // Each User can have many entries in the UserRole join table
68 | builder.HasMany().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
69 |
70 | // Relationships
71 |
72 | // Common Properties
73 |
74 | // Common fields
75 | // CreatedOn
76 | builder.Property(p => p.CreatedAt)
77 | .IsRequired();
78 |
79 | // CreatedByUserId
80 | builder.Property(p => p.CreatedByUserId)
81 | .IsRequired(false)
82 | .HasMaxLength(100);
83 |
84 | // ModifiedOn
85 | builder.Property(p => p.ModifiedAt)
86 | .IsRequired(false);
87 |
88 | // ModifiedByUserId
89 | builder.Property(p => p.ModifiedByUserId)
90 | .IsRequired(false)
91 | .HasMaxLength(100);
92 |
93 | builder.ToTable("application_users");
94 | }
95 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/ApplicationUserLoginConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Identity;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.Configurations;
6 |
7 | public sealed class ApplicationUserLoginConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Composite primary key consisting of the LoginProvider and the key to use
12 | // with that provider
13 | builder.HasKey(l => new { l.LoginProvider, l.ProviderKey });
14 |
15 | // Limit the size of the composite key columns due to common DB restrictions
16 | builder.Property(l => l.LoginProvider).HasMaxLength(128);
17 | builder.Property(l => l.ProviderKey).HasMaxLength(128);
18 |
19 | // Maps to the AspNetUserLogins table
20 | builder.ToTable("application_user_logins");
21 | }
22 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/ApplicationUserRoleConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Identity;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.Configurations;
6 |
7 | public sealed class ApplicationUserRoleConfiguration : IEntityTypeConfiguration
8 |
9 | {
10 | public void Configure(EntityTypeBuilder builder)
11 |
12 | {
13 | // Primary key
14 | builder.HasKey(r => new { r.UserId, r.RoleId });
15 |
16 | // Maps to the AspNetUserRoles table
17 | builder.ToTable("application_user_roles");
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/ApplicationUserTokenConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Identity;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.Configurations;
6 |
7 | public sealed class ApplicationUserTokenConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Composite primary key consisting of the UserId, LoginProvider and Name
12 | builder.HasKey(t => new { t.UserId, t.LoginProvider, t.Name });
13 |
14 | // Limit the size of the composite key columns due to common DB restrictions
15 | builder.Property(t => t.LoginProvider).HasMaxLength(191);
16 | builder.Property(t => t.Name).HasMaxLength(191);
17 |
18 | // Maps to the AspNetUserTokens table
19 | builder.ToTable("application_user_tokens");
20 | }
21 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/CategoryConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.EntityFramework.Configurations;
6 |
7 | public sealed class CategoryConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Id
12 | builder.HasKey(x => x.Id);
13 | builder.Property(x => x.Id).ValueGeneratedOnAdd();
14 |
15 | // Name
16 | builder.Property(x => x.Name)
17 | .IsRequired()
18 | .HasMaxLength(100);
19 |
20 | // Description
21 | builder.Property(x => x.Description)
22 | .IsRequired()
23 | .HasMaxLength(500);
24 |
25 | // Common Properties
26 |
27 | // CreatedAt
28 | builder.Property(p => p.CreatedAt)
29 | .IsRequired();
30 |
31 | // CreatedByUserId
32 | builder.Property(p => p.CreatedByUserId)
33 | .IsRequired(false)
34 | .HasMaxLength(100);
35 |
36 | // ModifiedAt
37 | builder.Property(p => p.ModifiedAt)
38 | .IsRequired(false);
39 |
40 | // ModifiedByUserId
41 | builder.Property(p => p.ModifiedByUserId)
42 | .IsRequired(false)
43 | .HasMaxLength(100);
44 |
45 | // Table Name
46 | builder.ToTable("categories");
47 | }
48 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/PlaceholderConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.EntityFramework.Configurations;
6 |
7 | public sealed class PlaceholderConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Id
12 | builder.HasKey(x => x.Id);
13 | builder.Property(x => x.Id).ValueGeneratedOnAdd();
14 |
15 | // Name
16 | builder.Property(x => x.Name)
17 | .IsRequired()
18 | .HasMaxLength(200);
19 |
20 | // CreatedAt
21 | builder.Property(p => p.CreatedAt)
22 | .IsRequired();
23 |
24 | // CreatedByUserId
25 | builder.Property(p => p.CreatedByUserId)
26 | .IsRequired(false)
27 | .HasMaxLength(100);
28 |
29 | // ModifiedAt
30 | builder.Property(p => p.ModifiedAt)
31 | .IsRequired(false);
32 |
33 | // ModifiedByUserId
34 | builder.Property(p => p.ModifiedByUserId)
35 | .IsRequired(false)
36 | .HasMaxLength(100);
37 |
38 | // Table Name
39 | builder.ToTable("placeholders");
40 | }
41 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/PromptCategoryConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.EntityFramework.Configurations;
6 |
7 | public sealed class PromptCategoryConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Id
12 | builder.HasKey(x => x.Id);
13 | builder.Property(x => x.Id).ValueGeneratedOnAdd();
14 |
15 | // Prompt Relationship
16 | builder.HasOne(x => x.Prompt)
17 | .WithMany(p => p.PromptCategories)
18 | .HasForeignKey(x => x.PromptId)
19 | .IsRequired();
20 |
21 | // Category Relationship
22 | builder.HasOne(x => x.Category)
23 | .WithMany(c => c.PromptCategories)
24 | .HasForeignKey(x => x.CategoryId)
25 | .IsRequired();
26 |
27 | // Unique Constraint for Prompt and Category Combination
28 | builder.HasIndex(x => new { x.PromptId, x.CategoryId })
29 | .IsUnique();
30 |
31 | // CreatedAt
32 | builder.Property(p => p.CreatedAt)
33 | .IsRequired();
34 |
35 | // CreatedByUserId
36 | builder.Property(p => p.CreatedByUserId)
37 | .IsRequired(false)
38 | .HasMaxLength(100);
39 |
40 | // ModifiedAt
41 | builder.Property(p => p.ModifiedAt)
42 | .IsRequired(false);
43 |
44 | // ModifiedByUserId
45 | builder.Property(p => p.ModifiedByUserId)
46 | .IsRequired(false)
47 | .HasMaxLength(100);
48 |
49 | // Table Name
50 | builder.ToTable("prompt_categories");
51 | }
52 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/PromptCommentConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.EntityFramework.Configurations;
6 |
7 | public sealed class UserPromptCommentConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Id
12 | builder.HasKey(x => x.Id);
13 | builder.Property(x => x.Id).ValueGeneratedOnAdd();
14 |
15 | // Level
16 | builder.Property(x => x.Level)
17 | .IsRequired();
18 |
19 | // Content
20 | builder.Property(x => x.Content)
21 | .IsRequired()
22 | .HasMaxLength(1000);
23 |
24 |
25 | // User Relationship
26 | builder.HasOne(x => x.User)
27 | .WithMany(u => u.PromptComments)
28 | .HasForeignKey(x => x.UserId);
29 |
30 | // Parent Comment Relationship
31 | builder.HasOne(x => x.ParentComment)
32 | .WithMany(pc => pc.ChildComments)
33 | .HasForeignKey(x => x.ParentCommentId)
34 | .IsRequired(false);
35 |
36 | // CreatedAt
37 | builder.Property(p => p.CreatedAt)
38 | .IsRequired();
39 |
40 | // CreatedByUserId
41 | builder.Property(p => p.CreatedByUserId)
42 | .IsRequired(false)
43 | .HasMaxLength(100);
44 |
45 | // ModifiedAt
46 | builder.Property(p => p.ModifiedAt)
47 | .IsRequired(false);
48 |
49 | // ModifiedByUserId
50 | builder.Property(p => p.ModifiedByUserId)
51 | .IsRequired(false)
52 | .HasMaxLength(100);
53 |
54 | // Table Name
55 | builder.ToTable("user_prompt_comments");
56 | }
57 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/PromptConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Entities;
2 | using AkilliPrompt.Domain.Identity;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
5 |
6 | namespace AkilliPrompt.Persistence.EntityFramework.Configurations;
7 |
8 | public sealed class PromptConfiguration : IEntityTypeConfiguration
9 | {
10 | public void Configure(EntityTypeBuilder builder)
11 | {
12 | // Id
13 | builder.HasKey(x => x.Id);
14 | builder.Property(x => x.Id).ValueGeneratedOnAdd();
15 |
16 | // Title
17 | builder.Property(x => x.Title)
18 | .IsRequired()
19 | .HasMaxLength(200);
20 |
21 | // Description
22 | builder.Property(x => x.Description)
23 | .IsRequired()
24 | .HasMaxLength(5000);
25 |
26 | // Content
27 | builder.Property(x => x.Content)
28 | .IsRequired();
29 |
30 | // ImageUrl
31 | builder.Property(x => x.ImageUrl)
32 | .HasMaxLength(1024)
33 | .IsRequired(false);
34 |
35 | // IsActive
36 | builder.Property(x => x.IsActive)
37 | .IsRequired()
38 | .HasDefaultValue(false);
39 |
40 | // LikeCount
41 | builder.Property(x => x.LikeCount)
42 | .IsRequired()
43 | .HasDefaultValue(0);
44 |
45 | builder.HasIndex(x => x.LikeCount)
46 | .IsDescending()
47 | .HasDatabaseName("IX_Prompts_LikeCount_Desc");
48 |
49 | // CreatorId
50 | builder.HasOne(x => x.Creator)
51 | .WithMany(x => x.CreatedPrompts)
52 | .HasForeignKey(x => x.CreatorId);
53 |
54 | // UserFavoritePrompts Relationship
55 | builder.HasMany(x => x.UserFavoritePrompts)
56 | .WithOne(ufp => ufp.Prompt)
57 | .HasForeignKey(ufp => ufp.PromptId);
58 |
59 | // UserLikePrompts Relationship
60 | builder.HasMany(x => x.UserLikePrompts)
61 | .WithOne(ulp => ulp.Prompt)
62 | .HasForeignKey(ulp => ulp.PromptId);
63 |
64 | // Placeholders Relationship
65 | builder.HasMany(x => x.Placeholders)
66 | .WithOne(p => p.Prompt)
67 | .HasForeignKey(p => p.PromptId);
68 |
69 | // PromptComments Relationship
70 | builder.HasMany(x => x.PromptComments)
71 | .WithOne(y => y.Prompt)
72 | .HasForeignKey(y => y.PromptId);
73 |
74 |
75 | // CreatedAt
76 | builder.Property(p => p.CreatedAt)
77 | .IsRequired();
78 |
79 | // CreatedByUserId
80 | builder.Property(p => p.CreatedByUserId)
81 | .IsRequired(false);
82 | //.HasMaxLength(150);
83 |
84 | // ModifiedAt
85 | builder.Property(p => p.ModifiedAt)
86 | .IsRequired(false);
87 |
88 | // ModifiedByUserId
89 | builder.Property(p => p.ModifiedByUserId)
90 | .IsRequired(false);
91 | //.HasMaxLength(150);
92 |
93 | // Table Name
94 | builder.ToTable("prompts");
95 | }
96 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/UserFavoritePromptConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.EntityFramework.Configurations;
6 |
7 | public sealed class UserFavoritePromptConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Id
12 | builder.HasKey(x => x.Id);
13 | builder.Property(x => x.Id).ValueGeneratedOnAdd();
14 |
15 |
16 | // User Relationship
17 | builder.HasOne(x => x.User)
18 | .WithMany(u => u.UserFavoritePrompts)
19 | .HasForeignKey(x => x.UserId);
20 |
21 | // Prompt Relationship
22 | builder.HasOne(x => x.Prompt)
23 | .WithMany(p => p.UserFavoritePrompts)
24 | .HasForeignKey(x => x.PromptId);
25 |
26 | // Unique Constraint for User and Prompt Combination
27 | builder.HasIndex(x => new { x.UserId, x.PromptId })
28 | .IsUnique(); //BUNU KONTROL ETMELİYİZ
29 |
30 |
31 | // CreatedAt
32 | builder.Property(p => p.CreatedAt)
33 | .IsRequired();
34 |
35 | // CreatedByUserId
36 | builder.Property(p => p.CreatedByUserId)
37 | .IsRequired(false)
38 | .HasMaxLength(100);
39 |
40 | // ModifiedAt
41 | builder.Property(p => p.ModifiedAt)
42 | .IsRequired(false);
43 |
44 | // ModifiedByUserId
45 | builder.Property(p => p.ModifiedByUserId)
46 | .IsRequired(false)
47 | .HasMaxLength(100);
48 |
49 | // Table Name
50 | builder.ToTable("user_favorite_prompts");
51 | }
52 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/UserLikePromptConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.EntityFramework.Configurations;
6 |
7 | public sealed class UserLikePromptConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Id
12 | builder.HasKey(x => x.Id);
13 | builder.Property(x => x.Id).ValueGeneratedOnAdd();
14 |
15 | // User Relationship
16 | builder.HasOne(x => x.User)
17 | .WithMany(u => u.UserLikePrompts)
18 | .HasForeignKey(x => x.UserId);
19 |
20 | // Prompt Relationship
21 | builder.HasOne(x => x.Prompt)
22 | .WithMany(p => p.UserLikePrompts)
23 | .HasForeignKey(x => x.PromptId);
24 |
25 | // Unique Constraint for User and Prompt Combination
26 | builder.HasIndex(x => new { x.UserId, x.PromptId })
27 | .IsUnique();
28 |
29 |
30 | // CreatedAt
31 | builder.Property(p => p.CreatedAt)
32 | .IsRequired();
33 |
34 | // CreatedByUserId
35 | builder.Property(p => p.CreatedByUserId)
36 | .IsRequired(false)
37 | .HasMaxLength(100);
38 |
39 | // ModifiedAt
40 | builder.Property(p => p.ModifiedAt)
41 | .IsRequired(false);
42 |
43 | // ModifiedByUserId
44 | builder.Property(p => p.ModifiedByUserId)
45 | .IsRequired(false)
46 | .HasMaxLength(100);
47 |
48 | // Table Name
49 | builder.ToTable("user_like_prompts");
50 | }
51 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Configurations/UserSocialMediaAccountConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace AkilliPrompt.Persistence.EntityFramework.Configurations;
6 |
7 | public sealed class UserSocialMediaAccountConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | // Id
12 | builder.HasKey(x => x.Id);
13 | builder.Property(x => x.Id).ValueGeneratedOnAdd();
14 |
15 | // SocialMediaType
16 | builder.Property(x => x.SocialMediaType)
17 | .HasColumnType("smallint")
18 | .HasConversion()
19 | .IsRequired();
20 |
21 | // Url
22 | builder.Property(x => x.Url)
23 | .IsRequired()
24 | .HasMaxLength(1024);
25 |
26 |
27 | // User Relationship
28 | builder.HasOne(x => x.User)
29 | .WithMany(u => u.UserSocialMediaAccounts)
30 | .HasForeignKey(x => x.UserId);
31 |
32 | // CreatedAt
33 | builder.Property(p => p.CreatedAt)
34 | .IsRequired();
35 |
36 | // CreatedByUserId
37 | builder.Property(p => p.CreatedByUserId)
38 | .IsRequired(false)
39 | .HasMaxLength(100);
40 |
41 | // ModifiedAt
42 | builder.Property(p => p.ModifiedAt)
43 | .IsRequired(false);
44 |
45 | // ModifiedByUserId
46 | builder.Property(p => p.ModifiedByUserId)
47 | .IsRequired(false)
48 | .HasMaxLength(100);
49 |
50 | // Table Name
51 | builder.ToTable("user_social_media_accounts");
52 | }
53 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Contexts/ApplicationDbContext.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Entities;
2 | using AkilliPrompt.Domain.Identity;
3 | using AkilliPrompt.Persistence.EntityFramework.Extensions;
4 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore;
6 |
7 | namespace AkilliPrompt.Persistence.EntityFramework.Contexts;
8 |
9 | public sealed class ApplicationDbContext : IdentityDbContext
10 | {
11 | public ApplicationDbContext(DbContextOptions options)
12 | : base(options)
13 | {
14 |
15 | }
16 |
17 | public DbSet Placeholders { get; set; }
18 | public DbSet Categories { get; set; }
19 | public DbSet Prompts { get; set; }
20 | public DbSet PromptCategories { get; set; }
21 | public DbSet UserSocialMediaAccounts { get; set; }
22 | public DbSet PromptComments { get; set; }
23 | public DbSet UserFavoritePrompts { get; set; }
24 | public DbSet UserLikePrompts { get; set; }
25 | public DbSet RefreshTokens { get; set; }
26 |
27 | protected override void OnModelCreating(ModelBuilder builder)
28 | {
29 | builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
30 |
31 | builder.ToSnakeCaseNames();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Contexts/ApplicationDbContextFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Design;
4 | using Microsoft.EntityFrameworkCore.Diagnostics;
5 | using Microsoft.Extensions.Configuration;
6 |
7 | namespace AkilliPrompt.Persistence.EntityFramework.Contexts;
8 |
9 | public sealed class ApplicationDbContextFactory : IDesignTimeDbContextFactory
10 | {
11 | public ApplicationDbContext CreateDbContext(string[] args)
12 | {
13 | var configuration = new ConfigurationBuilder()
14 | .SetBasePath(Directory.GetCurrentDirectory())
15 | .AddJsonFile("appsettings.json")
16 | .AddJsonFile("appsettings.Development.json", optional: true)
17 | .Build();
18 |
19 | var connectionString = configuration.GetConnectionString("DefaultConnection");
20 |
21 | var optionsBuilder = new DbContextOptionsBuilder();
22 |
23 | optionsBuilder.UseNpgsql(connectionString, b => b.MigrationsHistoryTable("__ef_migrations_history")).ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.PendingModelChangesWarning));
24 |
25 | return new ApplicationDbContext(optionsBuilder.Options);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Extensions/ConvertionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Text;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace AkilliPrompt.Persistence.EntityFramework.Extensions;
6 |
7 | public static class ConvertionExtensions
8 | {
9 | private static CultureInfo _culture;
10 |
11 | public static void ToSnakeCaseNames(this ModelBuilder modelBuilder)
12 | {
13 | _culture = CultureInfo.InvariantCulture;
14 |
15 | SetNames(modelBuilder, NamingConvention.SnakeCase);
16 | }
17 |
18 | public static void ToLowerCaseNames(this ModelBuilder modelBuilder)
19 | {
20 | _culture = CultureInfo.InvariantCulture;
21 |
22 | SetNames(modelBuilder, NamingConvention.LowerCase);
23 | }
24 |
25 | private static string? NameRewriter(this string name, NamingConvention naming)
26 | {
27 | if (string.IsNullOrEmpty(name)) return name;
28 |
29 | return naming == NamingConvention.SnakeCase
30 | ? SnakeCaseNameRewriter(name)
31 | : LowerCaseNameRewriter(name);
32 | }
33 |
34 | private enum NamingConvention
35 | {
36 | SnakeCase,
37 | LowerCase,
38 | }
39 |
40 | private static void SetNames(ModelBuilder modelBuilder, NamingConvention naming)
41 | {
42 | _culture = CultureInfo.InvariantCulture;
43 |
44 | foreach (var entity in modelBuilder.Model.GetEntityTypes())
45 | {
46 | entity.SetViewName(entity.GetViewName()?.NameRewriter(naming));
47 | entity.SetSchema(entity.GetSchema()?.NameRewriter(naming));
48 | entity.SetTableName(entity.GetTableName()?.NameRewriter(naming));
49 |
50 | foreach (var property in entity!.GetProperties())
51 | {
52 | property.SetColumnName(property.GetColumnName()?.NameRewriter(naming));
53 | }
54 |
55 | foreach (var key in entity.GetKeys())
56 | {
57 | key.SetName(key.GetName()?.NameRewriter(naming));
58 | }
59 |
60 | foreach (var key in entity.GetForeignKeys())
61 | {
62 | key.SetConstraintName(key.GetConstraintName()?.NameRewriter(naming));
63 | }
64 |
65 | foreach (var index in entity.GetIndexes())
66 | {
67 | index.SetDatabaseName(index.GetDatabaseName()?.NameRewriter(naming));
68 | }
69 | }
70 | }
71 |
72 | private static string? LowerCaseNameRewriter(string name)
73 | => name.ToLower(_culture);
74 |
75 | // https://github.com/efcore/EFCore.NamingConventions/blob/main/EFCore.NamingConventions/Internal/SnakeCaseNameRewriter.cs
76 | private static string SnakeCaseNameRewriter(string name)
77 | {
78 | var builder = new StringBuilder(name.Length + Math.Min(2, name.Length / 5));
79 | var previousCategory = default(UnicodeCategory?);
80 |
81 | for (var currentIndex = 0; currentIndex < name.Length; currentIndex++)
82 | {
83 | var currentChar = name[currentIndex];
84 | if (currentChar == '_')
85 | {
86 | builder.Append('_');
87 | previousCategory = null;
88 | continue;
89 | }
90 |
91 | var currentCategory = char.GetUnicodeCategory(currentChar);
92 | switch (currentCategory)
93 | {
94 | case UnicodeCategory.UppercaseLetter:
95 | case UnicodeCategory.TitlecaseLetter:
96 | if (previousCategory == UnicodeCategory.SpaceSeparator ||
97 | previousCategory == UnicodeCategory.LowercaseLetter ||
98 | previousCategory != UnicodeCategory.DecimalDigitNumber &&
99 | previousCategory != null &&
100 | currentIndex > 0 &&
101 | currentIndex + 1 < name.Length &&
102 | char.IsLower(name[currentIndex + 1]))
103 | {
104 | builder.Append('_');
105 | }
106 |
107 | currentChar = char.ToLower(currentChar, _culture);
108 | break;
109 |
110 | case UnicodeCategory.LowercaseLetter:
111 | case UnicodeCategory.DecimalDigitNumber:
112 | if (previousCategory == UnicodeCategory.SpaceSeparator)
113 | {
114 | builder.Append('_');
115 | }
116 | break;
117 |
118 | default:
119 | if (previousCategory != null)
120 | {
121 | previousCategory = UnicodeCategory.SpaceSeparator;
122 | }
123 | continue;
124 | }
125 |
126 | builder.Append(currentChar);
127 | previousCategory = currentCategory;
128 | }
129 |
130 | return builder.ToString().ToLower(_culture);
131 | }
132 | }
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Interceptors/EntityInterceptor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AkilliPrompt.Domain.Common;
3 | using AkilliPrompt.Persistence.Services;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.ChangeTracking;
6 | using Microsoft.EntityFrameworkCore.Diagnostics;
7 |
8 | namespace AkilliPrompt.Persistence.EntityFramework.Interceptors
9 | {
10 | public sealed class EntityInterceptor : SaveChangesInterceptor
11 | {
12 | private readonly ICurrentUserService _currentUserService;
13 |
14 | public EntityInterceptor(ICurrentUserService currentUserService)
15 | {
16 | _currentUserService = currentUserService;
17 | }
18 |
19 | public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result)
20 | {
21 | UpdateEntities(eventData.Context);
22 |
23 | return base.SavingChanges(eventData, result);
24 | }
25 |
26 | public override ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default)
27 | {
28 | UpdateEntities(eventData.Context);
29 |
30 | return base.SavingChangesAsync(eventData, result, cancellationToken);
31 | }
32 |
33 | private void UpdateEntities(DbContext? context)
34 | {
35 | if (context is null)
36 | return;
37 |
38 | foreach (var entry in context.ChangeTracker.Entries())
39 | {
40 | if (entry.State is EntityState.Added or EntityState.Modified || entry.HasChangedOwnedEntities())
41 | {
42 | var utcNow = DateTimeOffset.UtcNow;
43 |
44 | if (entry.State == EntityState.Added)
45 | {
46 | entry.Entity.CreatedByUserId = _currentUserService.UserId == Guid.Empty ? null : _currentUserService.UserId.ToString();
47 | entry.Entity.CreatedAt = utcNow;
48 | }
49 |
50 | if (entry.State == EntityState.Modified)
51 | {
52 | entry.Entity.ModifiedByUserId = _currentUserService.UserId == Guid.Empty ? null : _currentUserService.UserId.ToString();
53 | entry.Entity.ModifiedAt = utcNow;
54 | }
55 | }
56 | }
57 | }
58 |
59 | }
60 | public static class Extensions
61 | {
62 | public static bool HasChangedOwnedEntities(this EntityEntry entry) =>
63 | entry.References.Any(r =>
64 | r.TargetEntry != null &&
65 | r.TargetEntry.Metadata.IsOwned() &&
66 | (r.TargetEntry.State == EntityState.Added || r.TargetEntry.State == EntityState.Modified));
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/EntityFramework/Seeders/ApplicationRoleSeeder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AkilliPrompt.Domain.Identity;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
5 |
6 | namespace AkilliPrompt.Persistence.EntityFramework.Seeders;
7 |
8 | public sealed class ApplicationRoleSeeder : IEntityTypeConfiguration
9 | {
10 | public void Configure(EntityTypeBuilder builder)
11 | {
12 | var adminRole = new ApplicationRole
13 | {
14 | Id = new Guid("019358eb-f6cb-78c6-b59c-848777da66af"),
15 | Name = "Admin",
16 | NormalizedName = "ADMIN",
17 | ConcurrencyStamp = "019358ec-42e0-70ba-8049-655ecc8e2d2e",
18 | };
19 |
20 | var userRole = new ApplicationRole
21 | {
22 | Id = new Guid("019358ec-9d53-7785-a270-e22e10677a63"),
23 | Name = "User",
24 | NormalizedName = "USER",
25 | ConcurrencyStamp = "019358ec-aedc-742c-b677-a6b6bd8ef3bb",
26 | };
27 |
28 | builder.HasData(adminRole, userRole);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.Persistence/Services/ICurrentUserService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace AkilliPrompt.Persistence.Services;
4 |
5 | public interface ICurrentUserService
6 | {
7 | Guid UserId { get; }
8 | string IpAddress { get; }
9 | }
10 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.WebApi/AkilliPrompt.WebApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | f01eff83-4ba6-461e-bed7-5c82a97d7d34
8 | Linux
9 | ..\..
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | runtime; build; native; contentfiles; analyzers; buildtransitive
27 | all
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.WebApi/Attributes/CacheKeyPartAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.WebApi.Attributes;
2 |
3 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
4 | public sealed class CacheKeyPartAttribute : Attribute
5 | {
6 | // Optionally, add properties to control behavior, such as encoding
7 | public bool Encode { get; set; } = true;
8 |
9 | public string Prefix { get; set; } = string.Empty;
10 | }
11 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.WebApi/Attributes/CacheOptionsAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace AkilliPrompt.WebApi.Attributes;
2 |
3 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
4 | public sealed class CacheOptionsAttribute : Attribute
5 | {
6 | public TimeSpan? AbsoluteExpirationRelativeToNow { get; }
7 | public TimeSpan? SlidingExpiration { get; }
8 |
9 | public CacheOptionsAttribute(double absoluteExpirationMinutes = 30, double slidingExpirationMinutes = 10)
10 | {
11 | AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(absoluteExpirationMinutes);
12 | SlidingExpiration = TimeSpan.FromMinutes(slidingExpirationMinutes);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.WebApi/Behaviors/CachingBehavior.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Text.Json;
3 | using AkilliPrompt.Domain.Common;
4 | using AkilliPrompt.WebApi.Attributes;
5 | using AkilliPrompt.WebApi.Services;
6 | using MediatR;
7 | using Microsoft.Extensions.Caching.Distributed;
8 | using StackExchange.Redis;
9 |
10 | namespace AkilliPrompt.WebApi.Behaviors;
11 |
12 | public sealed class CachingBehavior : IPipelineBehavior
13 | where TRequest : ICacheable
14 | {
15 | private readonly IDistributedCache _cache;
16 | private readonly CacheKeyFactory _cacheKeyFactory;
17 | private readonly ILogger> _logger;
18 | private readonly IDatabase _redisDb;
19 | private static readonly DistributedCacheEntryOptions _defaultCacheOptions = new()
20 | {
21 | AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60),
22 | SlidingExpiration = TimeSpan.FromMinutes(15)
23 | };
24 |
25 | public CachingBehavior(
26 | IDistributedCache cache,
27 | CacheKeyFactory cacheKeyFactory,
28 | ILogger> logger,
29 | IConnectionMultiplexer redis)
30 | {
31 | _cache = cache;
32 | _cacheKeyFactory = cacheKeyFactory;
33 | _logger = logger;
34 | _redisDb = redis.GetDatabase();
35 | }
36 |
37 | public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
38 | {
39 | var cacheKey = _cacheKeyFactory.CreateCacheKey(request);
40 |
41 | var cachedData = await _cache.GetStringAsync(cacheKey, cancellationToken);
42 |
43 | if (!string.IsNullOrEmpty(cachedData))
44 | {
45 | _logger.LogInformation($"[Cache Hit] Key: {cacheKey}");
46 |
47 | return JsonSerializer.Deserialize(cachedData);
48 | }
49 |
50 | _logger.LogInformation($"[Cache Miss] Key: {cacheKey}. Fetching from database.");
51 |
52 | var response = await next();
53 |
54 | var serializedResponse = JsonSerializer.Serialize(response);
55 |
56 | // Retrieve custom cache options from attribute
57 | var cacheOptions = GetCacheOptionsFromAttribute() ?? _defaultCacheOptions;
58 |
59 | await _cache.SetStringAsync(cacheKey, serializedResponse, cacheOptions, cancellationToken);
60 |
61 | // Add cache key to group
62 | if (!string.IsNullOrEmpty(request.CacheGroup))
63 | {
64 | var groupSetKey = $"Group:{request.CacheGroup}";
65 |
66 | await _redisDb.SetAddAsync(groupSetKey, cacheKey);
67 | }
68 |
69 | return response;
70 | }
71 |
72 | private DistributedCacheEntryOptions? GetCacheOptionsFromAttribute()
73 | {
74 | var attribute = typeof(TRequest).GetCustomAttribute();
75 |
76 | if (attribute is null)
77 | return null;
78 |
79 | var options = new DistributedCacheEntryOptions();
80 |
81 | if (attribute.AbsoluteExpirationRelativeToNow.HasValue)
82 | options.AbsoluteExpirationRelativeToNow = attribute.AbsoluteExpirationRelativeToNow;
83 |
84 | if (attribute.SlidingExpiration.HasValue)
85 | options.SlidingExpiration = attribute.SlidingExpiration;
86 |
87 | return options;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.WebApi/Behaviors/ValidationBehavior.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using FluentValidation;
3 | using MediatR;
4 |
5 | namespace AkilliPrompt.WebApi.Behaviors;
6 |
7 | public sealed class ValidationBehavior : IPipelineBehavior
8 | where TRequest : IRequest
9 | {
10 | private readonly IEnumerable> _validators;
11 |
12 | public ValidationBehavior(IEnumerable> validators)
13 | {
14 | _validators = validators;
15 | }
16 |
17 | public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
18 | {
19 | if (_validators.Any())
20 | {
21 | var context = new ValidationContext(request);
22 |
23 | var validationResults = await Task.WhenAll(
24 | _validators.Select(v =>
25 | v.ValidateAsync(context, cancellationToken)));
26 |
27 | var failures = validationResults
28 | .Where(r => r.Errors.Any())
29 | .SelectMany(r => r.Errors)
30 | .ToList();
31 |
32 | if (failures.Any())
33 | throw new ValidationException(failures);
34 | }
35 |
36 | return await next();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.WebApi/Common/FluentValidation/EntityExistsValidator.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.Common;
2 | using AkilliPrompt.WebApi.Services;
3 | using FluentValidation;
4 |
5 | namespace AkilliPrompt.WebApi.Common.FluentValidation;
6 |
7 | ///
8 | /// Base validator to check existence of an entity by ID.
9 | ///
10 | /// Type of the entity.
11 | /// Type of the command containing the ID.
12 | public abstract class EntityExistsValidator : AbstractValidator
13 | where TEntity : EntityBase
14 | {
15 | private readonly IExistenceService _existenceService;
16 |
17 | public EntityExistsValidator(IExistenceService existenceService)
18 | {
19 | _existenceService = existenceService;
20 |
21 | RuleFor(e => GetEntityId(e))
22 | .NotEmpty()
23 | .WithMessage($"Lütfen geçerli bir {typeof(TEntity).Name} kimliği sağlayın.")
24 | .MustAsync(EntityExists)
25 | .WithMessage($"Belirtilen {typeof(TEntity).Name} mevcut değil.");
26 | }
27 |
28 | ///
29 | /// Extracts the entity ID from the command.
30 | ///
31 | /// The command containing the ID.
32 | /// The entity ID.
33 | protected abstract Guid GetEntityId(TCommand command);
34 |
35 | ///
36 | /// Checks if the entity exists using the ExistenceService.
37 | ///
38 | /// Entity ID.
39 | /// Cancellation token.
40 | /// True if exists; otherwise, false.
41 | private async Task EntityExists(Guid id, CancellationToken cancellationToken)
42 | {
43 | return await _existenceService.ExistsAsync(id, cancellationToken);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.WebApi/Configuration/SwaggerConfiguration.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.WebApi.Options;
2 | using Asp.Versioning;
3 | using Asp.Versioning.ApiExplorer;
4 | using Microsoft.OpenApi.Models;
5 | using Swashbuckle.AspNetCore.Filters;
6 |
7 | namespace AkilliPrompt.WebApi.Configuration;
8 |
9 | public static class SwaggerConfiguration
10 | {
11 | public static IServiceCollection AddSwaggerWithVersion(this IServiceCollection services)
12 | {
13 | services.AddSwaggerGen(setupAction =>
14 | {
15 | setupAction.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
16 | {
17 | Type = SecuritySchemeType.Http,
18 | Scheme = "bearer",
19 | BearerFormat = "JWT",
20 | Description = $"Input your Bearer token in this format - Bearer token to access this API",
21 | });
22 |
23 | setupAction.AddSecurityRequirement(new OpenApiSecurityRequirement
24 | {
25 | {
26 | new OpenApiSecurityScheme
27 | {
28 | Reference = new OpenApiReference
29 | {
30 | Type = ReferenceType.SecurityScheme,
31 | Id = "Bearer",
32 | },
33 | }, new List()
34 | },
35 | });
36 | });
37 |
38 | services.
39 | AddApiVersioning(options =>
40 | {
41 | options.ReportApiVersions = true;
42 | options.DefaultApiVersion = new ApiVersion(1, 0);
43 | options.AssumeDefaultVersionWhenUnspecified = true;
44 | options.ApiVersionReader = new UrlSegmentApiVersionReader(); //ApiVersionReader.Combine(new QueryStringApiVersionReader(), new UrlSegmentApiVersionReader(), new HeaderApiVersionReader("X-Api-Version"), new MediaTypeApiVersionReader("X-Api-Version"));
45 | })
46 | .AddApiExplorer(options =>
47 | {
48 | options.GroupNameFormat = "'v'VVV";
49 | options.SubstituteApiVersionInUrl = true;
50 | });
51 |
52 | services.ConfigureOptions();
53 |
54 | services.AddSwaggerExamplesFromAssemblyOf();
55 |
56 | return services;
57 | }
58 |
59 |
60 | public static IApplicationBuilder UseSwaggerWithVersion(this IApplicationBuilder app)
61 | {
62 | IApiVersionDescriptionProvider apiVersionDescriptionProvider = app.ApplicationServices.GetRequiredService();
63 |
64 | app.UseSwagger();
65 | app.UseSwaggerUI(options =>
66 | {
67 | foreach (ApiVersionDescription description in apiVersionDescriptionProvider.ApiVersionDescriptions)
68 | {
69 | options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"AkilliPrompt API {description.GroupName.ToUpperInvariant()}");
70 | }
71 | });
72 |
73 | return app;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.WebApi/DependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Text;
3 | using AkilliPrompt.Domain.Common;
4 | using AkilliPrompt.Domain.Identity;
5 | using AkilliPrompt.Domain.Settings;
6 | using AkilliPrompt.Persistence.EntityFramework.Contexts;
7 | using AkilliPrompt.Persistence.Services;
8 | using AkilliPrompt.WebApi.Behaviors;
9 | using AkilliPrompt.WebApi.Configuration;
10 | using AkilliPrompt.WebApi.Services;
11 | using AkilliPrompt.WebApi.V1.Auth.Commands.GoogleLogin;
12 | using FluentValidation;
13 | using IAPriceTrackerApp.WebApi.Services;
14 | using MediatR;
15 | using Microsoft.AspNetCore.Authentication.JwtBearer;
16 | using Microsoft.AspNetCore.Identity;
17 | using Microsoft.IdentityModel.Tokens;
18 | using StackExchange.Redis;
19 |
20 | namespace AkilliPrompt.WebApi;
21 |
22 | public static class DependencyInjection
23 | {
24 | public static IServiceCollection AddWebApi(this IServiceCollection services, IConfiguration configuration)
25 | {
26 | services.AddCors(options =>
27 | {
28 | options.AddPolicy("AllowAll",
29 | builder => builder
30 | .AllowAnyMethod()
31 | .AllowCredentials()
32 | .SetIsOriginAllowed((host) => true)
33 | .AllowAnyHeader());
34 | });
35 |
36 | services.AddSwaggerWithVersion();
37 | services.AddEndpointsApiExplorer();
38 |
39 | services.AddMemoryCache();
40 |
41 | services.AddProblemDetails();
42 | services.AddApiVersioning(
43 | options =>
44 | {
45 | options.ReportApiVersions = true;
46 | });
47 |
48 | services.AddHttpContextAccessor();
49 |
50 | services.AddScoped();
51 |
52 | services.Configure(
53 | configuration.GetSection(nameof(CloudflareR2Settings)));
54 |
55 | services.Configure(
56 | configuration.GetSection(nameof(JwtSettings)));
57 |
58 | services.Configure(
59 | configuration.GetSection(nameof(GoogleAuthSettings)));
60 |
61 |
62 | // Scoped Services
63 | services.AddScoped();
64 |
65 | services.AddIdentity(options =>
66 | {
67 | options.User.RequireUniqueEmail = true;
68 |
69 | options.Password.RequireNonAlphanumeric = false;
70 | options.Password.RequireUppercase = false;
71 | options.Password.RequireLowercase = false;
72 | options.Password.RequireDigit = false;
73 | options.Password.RequiredUniqueChars = 0;
74 | options.Password.RequiredLength = 6;
75 | })
76 | .AddEntityFrameworkStores()
77 | .AddDefaultTokenProviders();
78 |
79 | services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
80 |
81 | services.AddMediatR(config =>
82 | {
83 | config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
84 |
85 | config.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
86 |
87 | config.AddBehavior(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>));
88 |
89 | });
90 |
91 | // Configure Dragonfly as the caching provider
92 | services.AddStackExchangeRedisCache(options =>
93 | {
94 | options.Configuration = configuration.GetConnectionString("Dragonfly");
95 | options.InstanceName = "AkilliPrompt_"; // Optional: Use a specific instance name
96 | // Add any Dragonfly-specific configurations here
97 | // For example, if Dragonfly supports specific features or optimizations, configure them here
98 | });
99 |
100 | services.AddScoped();
101 |
102 | services.AddSingleton();
103 |
104 | services.AddScoped(typeof(IExistenceService<>), typeof(ExistenceManager<>));
105 |
106 | // Register Redis connection for advanced operations
107 | services.AddSingleton(sp =>
108 | ConnectionMultiplexer.Connect(configuration.GetConnectionString("Dragonfly")));
109 |
110 | services.AddScoped();
111 |
112 | services.AddAuthentication(options =>
113 | {
114 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
115 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
116 | options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
117 | options.DefaultSignOutScheme = JwtBearerDefaults.AuthenticationScheme;
118 | options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
119 | })
120 | .AddJwtBearer(options =>
121 | {
122 | var secretKey = configuration["JwtSettings:SecretKey"];
123 |
124 | if (string.IsNullOrEmpty(secretKey))
125 | throw new ArgumentNullException("JwtSettings:SecretKey is not set.");
126 |
127 | options.TokenValidationParameters = new TokenValidationParameters
128 | {
129 | ValidateIssuer = true,
130 | ValidateAudience = true,
131 | ValidateLifetime = true,
132 | ValidateIssuerSigningKey = true,
133 | ValidIssuer = configuration["JwtSettings:Issuer"],
134 | ValidAudience = configuration["JwtSettings:Audience"],
135 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
136 | ClockSkew = TimeSpan.Zero
137 | };
138 | });
139 |
140 | return services;
141 | }
142 |
143 | }
144 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.WebApi/Dockerfile:
--------------------------------------------------------------------------------
1 | # See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
2 |
3 | # This stage is used when running from VS in fast mode (Default for Debug configuration)
4 | FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
5 | USER $APP_UID
6 | WORKDIR /app
7 | EXPOSE 8080
8 | EXPOSE 8081
9 |
10 |
11 | # This stage is used to build the service project
12 | FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
13 | ARG BUILD_CONFIGURATION=Release
14 | WORKDIR /src
15 | COPY ["src/AkilliPrompt.WebApi/AkilliPrompt.WebApi.csproj", "src/AkilliPrompt.WebApi/"]
16 | RUN dotnet restore "./src/AkilliPrompt.WebApi/AkilliPrompt.WebApi.csproj"
17 | COPY . .
18 | WORKDIR "/src/src/AkilliPrompt.WebApi"
19 | RUN dotnet build "./AkilliPrompt.WebApi.csproj" -c $BUILD_CONFIGURATION -o /app/build
20 |
21 | # This stage is used to publish the service project to be copied to the final stage
22 | FROM build AS publish
23 | ARG BUILD_CONFIGURATION=Release
24 | RUN dotnet publish "./AkilliPrompt.WebApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
25 |
26 | # This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
27 | FROM base AS final
28 | WORKDIR /app
29 | COPY --from=publish /app/publish .
30 | ENTRYPOINT ["dotnet", "AkilliPrompt.WebApi.dll"]
--------------------------------------------------------------------------------
/src/AkilliPrompt.WebApi/Extensions/ApplicationBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Persistence.EntityFramework.Contexts;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace AkilliPrompt.WebApi.Extensions;
5 |
6 | public static class ApplicationBuilderExtensions
7 | {
8 | public static IApplicationBuilder ApplyMigrations(this IApplicationBuilder app)
9 | {
10 | using var scope = app.ApplicationServices.CreateScope();
11 |
12 | var dbContext = scope.ServiceProvider.GetRequiredService();
13 |
14 | if (dbContext.Database.GetPendingMigrations().Any())
15 | dbContext.Database.Migrate();
16 |
17 | return app;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/AkilliPrompt.WebApi/Filters/GlobalExceptionFilter.cs:
--------------------------------------------------------------------------------
1 | using AkilliPrompt.Domain.ValueObjects;
2 | using AkilliPrompt.WebApi.Helpers;
3 | using AkilliPrompt.WebApi.Models;
4 | using FluentValidation;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.AspNetCore.Mvc.Filters;
7 |
8 | namespace AkilliPrompt.WebApi.Filters;
9 |
10 | public class GlobalExceptionFilter : IExceptionFilter
11 | {
12 | private readonly ILogger _logger;
13 | public GlobalExceptionFilter(ILogger logger)
14 | {
15 | _logger = logger;
16 | }
17 | public void OnException(ExceptionContext context)
18 | {
19 | _logger.LogError(context.Exception, context.Exception.Message);
20 |
21 | context.ExceptionHandled = true;
22 |
23 | // Eğer hata bir doğrulama hatası ise
24 | if (context.Exception is ValidationException validationException)
25 | {
26 |
27 | var responseMessage = MessageHelper.GeneralValidationErrorMessage;
28 |
29 | var errors = validationException.Errors
30 | .GroupBy(e => e.PropertyName)
31 | .Select(g => new ValidationError(g.Key, g.Select(e => e.ErrorMessage)))
32 | .ToList();
33 |
34 | // 400 - Bad Request
35 | context.Result = new BadRequestObjectResult(ResponseDto