├── .gitattributes ├── .github └── workflows │ └── codeql.yml ├── .gitignore ├── README.md ├── SQLEFTableNotification.Console ├── Program.cs ├── SQLEFTableNotification.Console.csproj └── Services │ ├── ChangeTableService.cs │ └── SQLTableMonitorManager.cs ├── SQLEFTableNotification ├── SQLEFTableNotification.Api │ ├── Controllers │ │ ├── AccountAsyncController.cs │ │ ├── AccountController.cs │ │ ├── InfoController.cs │ │ ├── TokenController.cs │ │ ├── UserAsyncController.cs │ │ └── UserController.cs │ ├── ExceptionHandler.cs │ ├── Globals.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── SQLEFTableNotification.Api.csproj │ ├── Seed │ │ ├── accounts.json │ │ └── users.json │ ├── Settings │ │ └── LogglySettings.cs │ ├── Startup.cs │ ├── _nugets.txt │ ├── _readme.txt │ └── appsettings.json ├── SQLEFTableNotification.Domain │ ├── BaseDomain.cs │ ├── Domain │ │ ├── 2_t4DomainViewModelsGenerate.tt │ │ ├── AccountViewModel.cs │ │ └── UserViewModel.cs │ ├── Mapping │ │ └── MappingProfile.cs │ ├── SQLEFTableNotification.Domain.csproj │ ├── Service │ │ ├── 4_t4DomainServicesGenerate.tt │ │ ├── AccountService.cs │ │ ├── AccountServiceAsync.cs │ │ ├── Generic │ │ │ ├── GenericService.cs │ │ │ ├── GenericServiceAsync.cs │ │ │ ├── IService.cs │ │ │ └── IServiceAsync.cs │ │ ├── UserService.cs │ │ └── UserServiceAsync.cs │ └── _nugets.txt └── SQLEFTableNotification.Entity │ ├── 1_t4EntityHelpersGenerate.tt │ ├── BaseEntity.cs │ ├── CodeGeneratorUtility-readme.pdf │ ├── CodeGeneratorUtility.bat │ ├── Context │ ├── DBContextExtension.cs │ └── SQLEFTableNotificationContext.cs │ ├── Entity │ ├── Account.cs │ ├── ChangeTable.cs │ ├── EntityHelper.cs │ ├── User.cs │ └── UserChangeTable.cs │ ├── IEntityPk.cs │ ├── Migrations │ ├── 20180708205028_InitialCreate.Designer.cs │ ├── 20180708205028_InitialCreate.cs │ └── SQLEFTableNotificationContextModelSnapshot.cs │ ├── Model │ └── DBOperationType.cs │ ├── Repository │ ├── IRepository.cs │ ├── IRepositoryAsync.cs │ ├── Repository.cs │ └── RepositoryAsync.cs │ ├── SQLEFTableNotification.Entity.csproj │ ├── UnitofWork │ └── UnitofWork.cs │ ├── _dbfirst.txt │ └── _nugets.txt ├── SQLEFTableNotificationLib ├── Delegates │ └── ServiceDelegates.cs ├── Interfaces │ ├── IChangeTableService.cs │ └── IDBNotificationService.cs ├── Models │ ├── DBOperationType.cs │ ├── ErrorEventArgs.cs │ └── RecordChangedEventArgs.cs ├── SQLEFTableNotification.csproj ├── Services │ ├── ScheduledJobTimer.cs │ └── SqlDBNotificationService.cs └── SqlEFTableDependency.cd ├── SQLEFTableNotificationTests ├── SQLEFTableNotification.Tests.csproj └── Services │ └── SqlDBNotificationServiceTests.cs └── SQLTableNotification.sln /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '20 16 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | # Runner size impacts CodeQL analysis time. To learn more, please see: 27 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 28 | # - https://gh.io/supported-runners-and-hardware-resources 29 | # - https://gh.io/using-larger-runners 30 | # Consider using larger runners for possible analysis time improvements. 31 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 32 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 33 | permissions: 34 | actions: read 35 | contents: read 36 | security-events: write 37 | 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | language: [ 'csharp' ] 42 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] 43 | # Use only 'java' to analyze code written in Java, Kotlin or both 44 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 45 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 46 | 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@v3 50 | 51 | # Initializes the CodeQL tools for scanning. 52 | - name: Initialize CodeQL 53 | uses: github/codeql-action/init@v2 54 | with: 55 | languages: ${{ matrix.language }} 56 | # If you wish to specify custom queries, you can do so here or in a config file. 57 | # By default, queries listed here will override any specified in a config file. 58 | # Prefix the list here with "+" to use these queries and those in the config file. 59 | 60 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 61 | # queries: security-extended,security-and-quality 62 | 63 | 64 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 65 | # If this step fails, then you should remove it and run the build manually (see below) 66 | - name: Autobuild 67 | uses: github/codeql-action/autobuild@v2 68 | 69 | # ℹ️ Command-line programs to run using the OS shell. 70 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 71 | 72 | # If the Autobuild fails above, remove it and uncomment the following three lines. 73 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 74 | 75 | # - run: | 76 | # echo "Run, Build Application using script" 77 | # ./location_of_script_within_repo/buildscript.sh 78 | 79 | - name: Perform CodeQL Analysis 80 | uses: github/codeql-action/analyze@v2 81 | with: 82 | category: "/language:${{matrix.language}}" 83 | -------------------------------------------------------------------------------- /.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 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Building a Real-Time Database Notification Service with Change Tracking in C#](https://medium.com/@techbrainhub/building-a-real-time-database-notification-service-with-change-tracking-in-c-9512a2d14641) 2 | **Change Tracking In SQL Server** 3 | Change Tracking in SQL Server is a lightweight, built-in feature that helps track changes made to user tables in a database. It captures the fact that a row has been inserted, updated, or deleted, without storing the actual data changes. Instead, it records the minimum information needed to identify the modified rows. 4 | 5 | **Enable Change Tracking** 6 | Enabling Change Tracking in SQL Server involves a straightforward process. Here are the steps to enable Change Tracking for a specific table in a database: 7 | 8 | --SQL Script for enable change tracking at database level. 9 | ALTER DATABASE ECommerceDB 10 | SET CHANGE_TRACKING = ON 11 | (CHANGE_RETENTION = 1 DAYS, AUTO_CLEANUP = ON); 12 | 13 | --SQL Script for enable change tracking at table level. 14 | ALTER TABLE User 15 | ENABLE CHANGE_TRACKING 16 | WITH (TRACK_COLUMNS_UPDATED = ON ) 17 | 18 | _Retention Period (Optional): By default, SQL Server retains change tracking information for specified days_. 19 | 20 | **Monitor Change Tracking** 21 | After enabling Change Tracking, the system automatically maintains the change tracking information for the specified table(s). You can query the change tracking information periodically to synchronize changes with your application logic or for replication purposes. 22 | 23 | Remember that Change Tracking captures minimal information about the changes (PK values and version numbers), so you may need additional queries to get the full details of the modified rows if required. Also, enabling Change Tracking on a table incurs some performance overhead, albeit relatively low, depending on the frequency of data changes. 24 | 25 | **Access Tracked table records:** 26 | 27 | SELECT ct.* FROM CHANGETABLE(CHANGES ,) ct 28 | Get Change Tracking Current Version: 29 | 30 | SELECT ISNULL(CHANGE_TRACKING_CURRENT_VERSION(), 0) as VersionCount 31 | 32 | **Project Overview :** 33 | 34 | ![image](https://github.com/jatinrdave/SQLEFTableNotification/assets/15671321/eda1e961-48dd-4ebe-a110-10a197bc11b3) 35 | 36 | 37 | This database notification service is encapsulated within the SqlDBNotificationService class. This generic class is designed to work with any table entity, allowing flexibility and easy integration into various applications. The generic type constraint ensures that the entity class must be a reference type with a default constructor, providing a consistent structure for handling the table data. 38 | 39 | The SqlDBNotificationService class implements the IDBNotificationService interface, making it easy to integrate into existing projects. It also exposes events for error handling and receiving changed data. Service monitors database ChangeTable for specified interval and returns changed records for specified table till the last time monitored. 40 | 41 | 42 | **Usage:** 43 | ![image](https://github.com/jatinrdave/SQLEFTableNotification/assets/15671321/5b04b30b-8372-4408-91e4-77be04e337d4) 44 | 45 | * Initializing the Service: 46 | Developers can create an instance of the SqlDBNotificationService class by providing the table name, database connection string, and an optional initial version for resuming monitoring. 47 | * Subscribing to Events: 48 | Applications can subscribe to the OnError and OnChanged events to handle errors and receive notifications when changes occur in the table. 49 | * Starting and Stopping Monitoring: 50 | The service can be started by calling the StartNotify() method, which initiates the polling process. The service can be stopped using the StopNotify() method. 51 | 52 | **Features:** 53 | 54 | * Asynchronous Event-Based Model: The notification service employs a pub-sub model, meaning that when changes occur in the monitored table, the service sends asynchronous notifications to the registered subscribers (i.e., the application). 55 | * Query-Based Notifications: Unlike monitoring the entire table, our service allows the application to define a specific SQL query representing the data of interest. When relevant changes affect the query’s result set, a notification is triggered. 56 | * Low Resource Utilization: Our service optimizes resource consumption by avoiding constant polling. Instead, it leverages SQL Server’s Change Tracking, which efficiently tracks and delivers notifications only when necessary. 57 | * Flexible Initialization: Developers can specify the table to monitor, the connection string to the SQL Server database, an optional initial version for resuming monitoring, and a time interval for polling updates. 58 | * Error Handling and Resilience: The service includes robust error handling to ensure the application remains stable even in the face of unexpected exceptions. If an error threshold is reached, the service gracefully stops and notifies subscribers. 59 | * Easy Integration: The SqlDBNotificationService class implements the IDBNotificationService interface, making it easy to integrate into existing projects. It also exposes events for error handling and receiving changed data. 60 | 61 | **Conclusion:** 62 | 63 | By utilizing the powerful combination of C#, SQL Server Change Tracking, and our custom SqlDBNotificationService, developers can easily add real-time database monitoring to their applications. The service's efficient event-driven model ensures timely and accurate updates without excessive resource consumption, enhancing application responsiveness and overall user experience. 64 | -------------------------------------------------------------------------------- /SQLEFTableNotification.Console/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.IdentityModel.Abstractions; 4 | using SQLEFTableNotification.Console.Services; 5 | using SQLEFTableNotification.Entity.Entity; 6 | using SQLEFTableNotification.Interfaces; 7 | using SQLEFTableNotification.Services; 8 | 9 | public class MainProgram 10 | { 11 | /// 12 | /// The main entry point for the application. 13 | /// 14 | //[STAThread] 15 | public static void Main() 16 | { 17 | var serviceProvider = new ServiceCollection() 18 | .AddScoped(typeof(IChangeTableService<>), typeof(ChangeTableService<,>)) 19 | .AddScoped() 20 | .BuildServiceProvider(); 21 | 22 | ISQLTableMonitorManager tableMonitorManager = serviceProvider.GetRequiredService(); 23 | Task.Run( async()=> await tableMonitorManager.Invoke()).Wait(); 24 | } 25 | 26 | 27 | } -------------------------------------------------------------------------------- /SQLEFTableNotification.Console/SQLEFTableNotification.Console.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /SQLEFTableNotification.Console/Services/ChangeTableService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.EntityFrameworkCore; 3 | using SQLEFTableNotification.Domain; 4 | using SQLEFTableNotification.Domain.Service; 5 | using SQLEFTableNotification.Entity; 6 | using SQLEFTableNotification.Entity.Entity; 7 | using SQLEFTableNotification.Entity.UnitofWork; 8 | using SQLEFTableNotification.Interfaces; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace SQLEFTableNotification.Console.Services 16 | { 17 | public class ChangeTableService : GenericServiceAsync, IChangeTableService where T : BaseEntity where TView : BaseDomain 18 | { 19 | public ChangeTableService(IUnitOfWork unitOfWork,IMapper mapper) 20 | : base(unitOfWork, mapper) 21 | { 22 | 23 | } 24 | 25 | public async Task> GetRecords(string CommandText) 26 | { 27 | var record = await _unitOfWork.GetRepositoryAsync().GetModelWithRawSql(CommandText).ToListAsync(); 28 | return record; 29 | } 30 | 31 | public List GetRecordsSync(string CommandText) 32 | { 33 | return Task.Run(() => GetRecords(CommandText)).Result; 34 | } 35 | 36 | public async Task GetRecordCount(string CommandText) 37 | { 38 | var record = await _unitOfWork.GetRepositoryAsync().GetModelWithRawSql(CommandText).FirstOrDefaultAsync(); ; 39 | return record != null ? record.VersionCount : 0; 40 | } 41 | 42 | public long GetRecordCountSync(string CommandText) 43 | { 44 | return Task.Run(() => GetRecordCount(CommandText)).Result; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SQLEFTableNotification.Console/Services/SQLTableMonitorManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using SQLEFTableNotification.Domain; 3 | using SQLEFTableNotification.Domain.Service; 4 | using SQLEFTableNotification.Entity; 5 | using SQLEFTableNotification.Entity.Entity; 6 | using SQLEFTableNotification.Interfaces; 7 | using SQLEFTableNotification.Services; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace SQLEFTableNotification.Console.Services 15 | { 16 | public interface ISQLTableMonitorManager 17 | { 18 | Task Invoke(); 19 | } 20 | 21 | public class SQLTableMonitorManager : ISQLTableMonitorManager 22 | { 23 | private readonly IChangeTableService _changeTableService; 24 | private readonly UserServiceAsync _userService; 25 | 26 | public SQLTableMonitorManager(IChangeTableService changeTableService,UserServiceAsync userService) 27 | { 28 | _changeTableService = changeTableService; 29 | _userService = userService; 30 | } 31 | 32 | public async Task Invoke() 33 | { 34 | string connectionString = "";//From Config file. 35 | IDBNotificationService sqlDBNotificationService = new SqlDBNotificationService("User", connectionString, _changeTableService, -1, TimeSpan.FromHours(1), "API"); 36 | await sqlDBNotificationService.StartNotify(); 37 | sqlDBNotificationService.OnChanged += SqlDBNotificationService_OnChanged; 38 | sqlDBNotificationService.OnError += SqlDBNotificationService_OnError; 39 | } 40 | 41 | private async void SqlDBNotificationService_OnError(object sender, SQLEFTableNotification.Models.ErrorEventArgs e) 42 | { 43 | //log error 44 | } 45 | 46 | private async void SqlDBNotificationService_OnChanged(object sender, SQLEFTableNotification.Models.RecordChangedEventArgs e) 47 | { 48 | foreach (var record in e.Entities) 49 | { 50 | var user = await _userService.GetOne(record.Id); 51 | //Perform action. 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Controllers/AccountAsyncController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Logging; 4 | using Serilog; 5 | using SQLEFTableNotification.Domain; 6 | using SQLEFTableNotification.Domain.Service; 7 | using SQLEFTableNotification.Entity; 8 | using SQLEFTableNotification.Entity.Context; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace SQLEFTableNotification.Api.Controllers 14 | { 15 | [ApiVersion("1.0")] 16 | [Route("api/[controller]")] 17 | //[Route("api/v{version:apiVersion}/[controller]")] 18 | [ApiController] 19 | public class AccountAsyncController : ControllerBase 20 | { 21 | private readonly AccountServiceAsync _accountServiceAsync; 22 | public AccountAsyncController(AccountServiceAsync accountServiceAsync) 23 | { 24 | _accountServiceAsync = accountServiceAsync; 25 | } 26 | 27 | 28 | //get all 29 | [Authorize] 30 | [HttpGet] 31 | public async Task GetAll() 32 | { 33 | var items = await _accountServiceAsync.GetAll(); 34 | return Ok(items); 35 | } 36 | 37 | //get by predicate example 38 | //get all active by name 39 | [Authorize] 40 | [HttpGet("GetActiveByName/{name}")] 41 | public async Task GetActiveByName(string name) 42 | { 43 | var items = await _accountServiceAsync.Get(a => a.IsActive && a.Name == name); 44 | return Ok(items); 45 | } 46 | 47 | //get one 48 | [Authorize] 49 | [HttpGet("{id}")] 50 | public async Task GetById(int id) 51 | { 52 | var item = await _accountServiceAsync.GetOne(id); 53 | if (item == null) 54 | { 55 | Log.Error("GetById({ ID}) NOT FOUND", id); 56 | return NotFound(); 57 | } 58 | 59 | return Ok(item); 60 | } 61 | 62 | //add 63 | [Authorize(Roles = "Administrator")] 64 | [HttpPost] 65 | public async Task Create([FromBody] AccountViewModel account) 66 | { 67 | if (account == null) 68 | return BadRequest(); 69 | 70 | var id = await _accountServiceAsync.Add(account); 71 | return Created($"api/Account/{id}", id); //HTTP201 Resource created 72 | } 73 | 74 | //update 75 | [Authorize(Roles = "Administrator")] 76 | [HttpPut("{id}")] 77 | public async Task Update(int id, [FromBody] AccountViewModel account) 78 | { 79 | if (account == null || account.Id != id) 80 | return BadRequest(); 81 | 82 | int retVal = await _accountServiceAsync.Update(account); 83 | if (retVal == 0) 84 | return StatusCode(304); //Not Modified 85 | else if (retVal == -1) 86 | return StatusCode(412, "DbUpdateConcurrencyException"); //412 Precondition Failed - concurrency 87 | else 88 | return Accepted(account); 89 | } 90 | 91 | 92 | //delete 93 | [Authorize(Roles = "Administrator")] 94 | [HttpDelete("{id}")] 95 | public async Task Delete(int id) 96 | { 97 | int retVal = await _accountServiceAsync.Remove(id); 98 | if (retVal == 0) 99 | return NotFound(); //Not Found 404 100 | else if (retVal == -1) 101 | return StatusCode(412, "DbUpdateConcurrencyException"); //Precondition Failed - concurrency 102 | else 103 | return NoContent(); //No Content 204 104 | } 105 | } 106 | } 107 | 108 | 109 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Logging; 4 | using Serilog; 5 | using SQLEFTableNotification.Domain; 6 | using SQLEFTableNotification.Domain.Service; 7 | using SQLEFTableNotification.Entity; 8 | using SQLEFTableNotification.Entity.Context; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | 12 | namespace SQLEFTableNotification.Api.Controllers 13 | { 14 | [ApiVersion("1.0")] 15 | [Route("api/[controller]")] 16 | //[Route("api/v{version:apiVersion}/[controller]")] 17 | [ApiController] 18 | public class AccountController : ControllerBase 19 | { 20 | private readonly AccountService _accountService; 21 | public AccountController(AccountService accountService) 22 | { 23 | _accountService = accountService; 24 | } 25 | 26 | //get all 27 | [Authorize] 28 | [HttpGet] 29 | public IEnumerable GetAll() 30 | { 31 | //Log.Information("Log: Log.Information"); 32 | //Log.Warning("Log: Log.Warning"); 33 | //Log.Error("Log: Log.Error"); 34 | //Log.Fatal("Log: Log.Fatal"); 35 | var test = _accountService.DoNothing(); 36 | var items = _accountService.GetAll(); 37 | return items; 38 | } 39 | 40 | //get by predicate example 41 | //get all active by name 42 | [Authorize] 43 | [HttpGet("GetActiveByName/{name}")] 44 | public IActionResult GetActiveByName(string name) 45 | { 46 | var items = _accountService.Get(a => a.IsActive && a.Name == name); 47 | return Ok(items); 48 | } 49 | 50 | //get one 51 | [Authorize] 52 | [HttpGet("{id}")] 53 | public IActionResult GetById(int id) 54 | { 55 | var item = _accountService.GetOne(id); 56 | if (item == null) 57 | { 58 | Log.Error("GetById({ ID}) NOT FOUND", id); 59 | return NotFound(); 60 | } 61 | 62 | return Ok(item); 63 | } 64 | 65 | //add 66 | [Authorize(Roles = "Administrator")] 67 | [HttpPost] 68 | public IActionResult Create([FromBody] AccountViewModel account) 69 | { 70 | if (account == null) 71 | return BadRequest(); 72 | 73 | var id = _accountService.Add(account); 74 | return Created($"api/Account/{id}", id); //HTTP201 Resource created 75 | } 76 | 77 | //update 78 | [Authorize(Roles = "Administrator")] 79 | [HttpPut("{id}")] 80 | public IActionResult Update(int id, [FromBody] AccountViewModel account) 81 | { 82 | if (account == null || account.Id != id) 83 | return BadRequest(); 84 | 85 | int retVal = _accountService.Update(account); 86 | if (retVal == 0) 87 | return StatusCode(304); //Not Modified 88 | else if (retVal == -1) 89 | return StatusCode(412, "DbUpdateConcurrencyException"); //412 Precondition Failed - concurrency 90 | else 91 | return Accepted(account); 92 | } 93 | 94 | //delete 95 | [Authorize(Roles = "Administrator")] 96 | [HttpDelete("{id}")] 97 | public IActionResult Delete(int id) 98 | { 99 | int retVal = _accountService.Remove(id); 100 | if (retVal == 0) 101 | return NotFound(); //Not Found 404 102 | else if (retVal == -1) 103 | return StatusCode(412, "DbUpdateConcurrencyException"); //Precondition Failed - concurrency 104 | else 105 | return NoContent(); //No Content 204 106 | } 107 | } 108 | } 109 | 110 | 111 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Controllers/InfoController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Threading.Tasks; 10 | 11 | /// 12 | /// This controller is an example of API versioning 13 | /// 14 | 15 | namespace SQLEFTableNotification.Api.Controllers.v1 16 | { 17 | [ApiVersion("1.0")] 18 | [Route("api/[controller]")] 19 | [Route("api/v{version:apiVersion}/[controller]")] 20 | [ApiController] 21 | public class InfoController : ControllerBase 22 | { 23 | 24 | public IConfiguration Configuration { get; } 25 | public InfoController(IConfiguration configuration) 26 | { 27 | Configuration = configuration; 28 | } 29 | 30 | //get info 31 | [AllowAnonymous] 32 | [HttpGet] 33 | [Produces("application/json")] 34 | public IActionResult ApiInfo() 35 | { 36 | 37 | var migration = Configuration["ConnectionStrings:UseMigrationService"]; 38 | var seed = Configuration["ConnectionStrings:UseSeedService"]; 39 | var memorydb = Configuration["ConnectionStrings:UseInMemoryDatabase"]; 40 | var connstring = Configuration["ConnectionStrings:SQLEFTableNotificationDB"]; 41 | //var is4ip = Configuration["Authentication:IdentityServer4IP"]; // only full version 42 | 43 | var controlers = MvcHelper.GetControllerMethodsNames(); 44 | return Content("" + 45 | 46 | "
" + 47 | "

SQLEFTableNotification Api v.1

" + 48 | "

Created with RestApiN v.7.0

" + 49 | "NET.Core Api REST service started!
" + 50 | "appsettings.json configuration:
" + 51 | "
  • .NET 7.0
  • " + 52 | "
  • Use Migration Service: " + migration + "
  • " + 53 | "
  • Use Seed Service: " + seed + "
  • " + 54 | "
  • Use InMemory Database: " + memorydb + "
  • " + 55 | "
  • Authentication Type: JWT
  • " + 56 | "
  • Connection String: " + connstring + "
" + 57 | "
  • Logs location: SQLEFTableNotification.Api\\Logs
  • " + 58 | "Download full version with tests and T4
    " + 59 | "More instructions and more features " + 60 | "
    " + 61 | 62 | "
    " + 63 | 64 | "
    " + 65 | "

    API controlers and methods

    " + 66 | "
      " + controlers + "
    " + 67 | "

    " + 68 | "
    " + 69 | "
    " + 70 | "

    API services and patterns

    " + 71 | "

    • Dependency Injection
    • Repository and Unit of Work Patterns
    • Generic services
    • Automapper
    • Sync and Async calls
    • Generic exception handler
    • Serilog logging with Console and File sinks
    • Seed from json objects
    • JWT authorization and authentication
    " + 72 | "
    " + 73 | "
    " + 74 | "

    API projects

    " + 75 | "
    • Api
    • Domain
    • Entity
    " + 76 | "
    " + 77 | 78 | "
    " + 79 | "" 80 | , "text/html"); 81 | 82 | } 83 | 84 | } 85 | 86 | public class MvcHelper 87 | { 88 | private static List GetSubClasses() 89 | { 90 | return Assembly.GetCallingAssembly().GetTypes().Where( 91 | type => type.IsSubclassOf(typeof(T))).ToList(); 92 | } 93 | 94 | public static string GetControllerMethodsNames() 95 | { 96 | List cmdtypes = GetSubClasses(); 97 | var controlersInfo = ""; 98 | foreach (Type ctrl in cmdtypes) 99 | { 100 | var methodsInfo = ""; 101 | const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; 102 | MemberInfo[] methodName = ctrl.GetMethods(flags); 103 | foreach (MemberInfo method in methodName) 104 | { 105 | if (method.DeclaringType.ToString() == ctrl.UnderlyingSystemType.ToString()) 106 | methodsInfo += "
  • " + method.Name.ToString() + "
  • "; 107 | } 108 | controlersInfo += "
  • " + ctrl.Name.Replace("Controller", "") + "
      " + methodsInfo + "
  • "; 109 | } 110 | return controlersInfo; 111 | } 112 | } 113 | 114 | } 115 | 116 | 117 | namespace SQLEFTableNotification.Api.Controllers.v2 118 | { 119 | [ApiVersion("2.0")] 120 | [Route("api/v{version:apiVersion}/[controller]")] 121 | [ApiController] 122 | public class InfoController : ControllerBase 123 | { 124 | 125 | public IConfiguration Configuration { get; } 126 | public InfoController(IConfiguration configuration) 127 | { 128 | Configuration = configuration; 129 | } 130 | 131 | //get info 132 | [AllowAnonymous] 133 | [HttpGet] 134 | [Produces("application/json")] 135 | public IActionResult ApiInfo() 136 | { 137 | 138 | var migration = Configuration["ConnectionStrings:UseMigrationService"]; 139 | var seed = Configuration["ConnectionStrings:UseSeedService"]; 140 | var memorydb = Configuration["ConnectionStrings:UseInMemoryDatabase"]; 141 | var connstring = Configuration["ConnectionStrings:SQLEFTableNotificationDB"]; 142 | var authentication = Configuration["Authentication:UseIdentityServer4"]; 143 | var is4ip = Configuration["Authentication:IdentityServer4IP"]; 144 | 145 | var controlers = MvcHelper.GetControllerMethodsNames(); 146 | return Content("" + 147 | 148 | "
    " + 149 | "

    SQLEFTableNotification Api v.1

    " + 150 | "

    Created with RestApiN v.5.0

    " + 151 | "REST Api service started!
    " + 152 | "appsettings.json configuration:
    " + 153 | "
    • .NET 5.0
    • " + 154 | "
    • Use Migration Service: " + migration + "
    • " + 155 | "
    • Use Seed Service: " + seed + "
    • " + 156 | "
    • Use InMemory Database: " + memorydb + "
    • " + 157 | "
    • Authentication Type: " + (authentication == "True" ? "IS4" : "JWT") + "
    • " + 158 | (authentication == "True" ? "
    • IdentityServer4IP: " + is4ip + "
    • " : "") + 159 | "
    • Connection String: " + connstring + "
    " + 160 | "
    " + 161 | 162 | "
    " + 163 | 164 | "
    " + 165 | "

    API controlers and methods

    " + 166 | "
      " + controlers + "
    " + 167 | "

    " + 168 | "
    " + 169 | "
    " + 170 | "

    API services and patterns

    " + 171 | "

    • Dependency Injection
    • Repository and Unit of Work Patterns
    • Generic services
    • Automapper
    • Sync and Async calls
    • Generic exception handler
    • Serilog logging with Console and File sinks
    • Seed from json objects
    • JWT authorization and authentication
    " + 172 | "
    " + 173 | "
    " + 174 | "

    API projects

    " + 175 | "
    • Api
    • Domain
    • Entity
    " + 176 | "
    " + 177 | 178 | "
    " + 179 | "" 180 | , "text/html"); 181 | 182 | } 183 | 184 | } 185 | 186 | public class MvcHelper 187 | { 188 | private static List GetSubClasses() 189 | { 190 | return Assembly.GetCallingAssembly().GetTypes().Where( 191 | type => type.IsSubclassOf(typeof(T))).ToList(); 192 | } 193 | 194 | public static string GetControllerMethodsNames() 195 | { 196 | List cmdtypes = GetSubClasses(); 197 | var controlersInfo = ""; 198 | foreach (Type ctrl in cmdtypes) 199 | { 200 | var methodsInfo = ""; 201 | const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; 202 | MemberInfo[] methodName = ctrl.GetMethods(flags); 203 | foreach (MemberInfo method in methodName) 204 | { 205 | if (method.DeclaringType.ToString() == ctrl.UnderlyingSystemType.ToString()) 206 | methodsInfo += "
  • " + method.Name.ToString() + "
  • "; 207 | } 208 | controlersInfo += "
  • " + ctrl.Name.Replace("Controller", "") + "
      " + methodsInfo + "
  • "; 209 | } 210 | return controlersInfo; 211 | } 212 | } 213 | 214 | } 215 | 216 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Controllers/TokenController.cs: -------------------------------------------------------------------------------- 1 | 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.IdentityModel.Tokens; 6 | using SQLEFTableNotification.Domain; 7 | using SQLEFTableNotification.Domain.Service; 8 | using SQLEFTableNotification.Entity; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.IdentityModel.Tokens.Jwt; 12 | using System.Linq; 13 | using System.Security.Claims; 14 | using System.Text; 15 | 16 | namespace JWT.Controllers 17 | { 18 | [ApiVersion("1.0")] 19 | [Route("api/[controller]")] 20 | //[Route("api/v{version:apiVersion}/[controller]")] 21 | [ApiController] 22 | public class TokenController : Controller 23 | { 24 | private IConfiguration _config; 25 | private readonly IService _userService; 26 | 27 | public TokenController(IConfiguration config, IService userService) 28 | { 29 | _config = config; 30 | _userService = userService; 31 | } 32 | 33 | [AllowAnonymous] 34 | [HttpPost] 35 | public IActionResult Create([FromBody] LoginModel login) 36 | { 37 | IActionResult response = Unauthorized(); 38 | var user = Authenticate(login); 39 | 40 | if (user != null) 41 | { 42 | var tokenString = BuildToken(user); 43 | response = Ok(new { token = tokenString }); 44 | } 45 | 46 | return response; 47 | } 48 | 49 | private string BuildToken(UserModel user) 50 | { 51 | 52 | List claims = new List(); 53 | claims.Add(new Claim(JwtRegisteredClaimNames.Sub, user.Name)); 54 | claims.Add(new Claim(JwtRegisteredClaimNames.Email, user.Email)); 55 | claims.Add(new Claim(JwtRegisteredClaimNames.Birthdate, user.Birthdate.ToString("yyyy-MM-dd"))); 56 | claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())); 57 | //attach roles 58 | foreach (string role in user.Roles) 59 | { 60 | claims.Add(new Claim(ClaimTypes.Role, role)); 61 | } 62 | 63 | var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"])); 64 | var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); 65 | 66 | var token = new JwtSecurityToken( 67 | _config["Jwt:Issuer"], 68 | _config["Jwt:Issuer"], 69 | claims, 70 | expires: DateTime.Now.AddMinutes(60), //60 min expiry and a client monitor token quality and should request new token with this one expiries 71 | signingCredentials: creds); 72 | 73 | return new JwtSecurityTokenHandler().WriteToken(token); 74 | } 75 | 76 | //Authenticates login information, retrieves authorization infomation (roles) 77 | private UserModel Authenticate(LoginModel login) 78 | { 79 | UserModel user = null; 80 | 81 | var userView = _userService.Get(x => x.UserName == login.Username).SingleOrDefault(); 82 | if (userView != null && userView.Password == login.Password) 83 | { 84 | user = new UserModel { Name = userView.FirstName + " " + userView.LastName, Email = userView.Email, Roles = new string[] { } }; 85 | foreach (string role in userView.Roles) user.Roles = new List(user.Roles) { role }.ToArray(); 86 | if (userView.IsAdminRole) user.Roles = new List(user.Roles) { "Administrator" }.ToArray(); 87 | } 88 | 89 | return user; 90 | } 91 | 92 | public class LoginModel 93 | { 94 | public string Username { get; set; } 95 | public string Password { get; set; } 96 | } 97 | 98 | private class UserModel 99 | { 100 | public string Name { get; set; } 101 | public string Email { get; set; } 102 | public DateTime Birthdate { get; set; } 103 | public string[] Roles { get; set; } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Controllers/UserAsyncController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Logging; 4 | using Serilog; 5 | using SQLEFTableNotification.Domain; 6 | using SQLEFTableNotification.Domain.Service; 7 | using SQLEFTableNotification.Entity; 8 | using SQLEFTableNotification.Entity.Context; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace SQLEFTableNotification.Api.Controllers 14 | { 15 | [ApiVersion("1.0")] 16 | [Route("api/[controller]")] 17 | //[Route("api/v{version:apiVersion}/[controller]")] 18 | [ApiController] 19 | public class UserAsyncController : ControllerBase 20 | { 21 | private readonly UserServiceAsync _userServiceAsync; 22 | public UserAsyncController(UserServiceAsync userServiceAsync) 23 | { 24 | _userServiceAsync = userServiceAsync; 25 | } 26 | 27 | 28 | //get all 29 | [Authorize] 30 | [HttpGet] 31 | public async Task> GetAll() 32 | { 33 | var items = await _userServiceAsync.GetAll(); 34 | return items; 35 | } 36 | 37 | //get by predicate example 38 | //get all active by username 39 | [Authorize] 40 | [HttpGet("GetActiveByFirstName/{firstname}")] 41 | public async Task GetActiveByFirstName(string firstname) 42 | { 43 | var items = await _userServiceAsync.Get(a => a.IsActive && a.FirstName == firstname); 44 | return Ok(items); 45 | } 46 | 47 | //get one 48 | [Authorize] 49 | [HttpGet("{id}")] 50 | public async Task GetById(int id) 51 | { 52 | var item = await _userServiceAsync.GetOne(id); 53 | if (item == null) 54 | { 55 | Log.Error("GetById({ ID}) NOT FOUND", id); 56 | return NotFound(); 57 | } 58 | 59 | return Ok(item); 60 | } 61 | 62 | //add 63 | [Authorize(Roles = "Administrator")] 64 | [HttpPost] 65 | public async Task Create([FromBody] UserViewModel user) 66 | { 67 | if (user == null) 68 | return BadRequest(); 69 | 70 | var id = await _userServiceAsync.Add(user); 71 | return Created($"api/User/{id}", id); //HTTP201 Resource created 72 | } 73 | 74 | //update 75 | [Authorize(Roles = "Administrator")] 76 | [HttpPut("{id}")] 77 | public async Task Update(int id, [FromBody] UserViewModel user) 78 | { 79 | if (user == null || user.Id != id) 80 | return BadRequest(); 81 | 82 | int retVal = await _userServiceAsync.Update(user); 83 | if (retVal == 0) 84 | return StatusCode(304); //Not Modified 85 | else if (retVal == -1) 86 | return StatusCode(412, "DbUpdateConcurrencyException"); //412 Precondition Failed - concurrency 87 | else 88 | return Accepted(user); 89 | } 90 | 91 | //delete 92 | [Authorize(Roles = "Administrator")] 93 | [HttpDelete("{id}")] 94 | public async Task Delete(int id) 95 | { 96 | int retVal = await _userServiceAsync.Remove(id); 97 | if (retVal == 0) 98 | return NotFound(); //Not Found 404 99 | else if (retVal == -1) 100 | return StatusCode(412, "DbUpdateConcurrencyException"); //Precondition Failed - concurrency 101 | else 102 | return NoContent(); //No Content 204 103 | } 104 | 105 | } 106 | } 107 | 108 | 109 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Controllers/UserController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Logging; 4 | using Serilog; 5 | using SQLEFTableNotification.Domain; 6 | using SQLEFTableNotification.Domain.Service; 7 | using SQLEFTableNotification.Entity; 8 | using SQLEFTableNotification.Entity.Context; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | 12 | namespace SQLEFTableNotification.Api.Controllers 13 | { 14 | [ApiVersion("1.0")] 15 | [Route("api/[controller]")] 16 | //[Route("api/v{version:apiVersion}/[controller]")] 17 | [ApiController] 18 | public class UserController : ControllerBase 19 | { 20 | private readonly UserService _userService; 21 | public UserController(UserService userService) 22 | { 23 | _userService = userService; 24 | } 25 | 26 | //get all 27 | [Authorize] 28 | [HttpGet] 29 | public IEnumerable GetAll() 30 | { 31 | var test = _userService.DoNothing(); 32 | var items = _userService.GetAll(); 33 | return items; 34 | } 35 | 36 | //get by predicate example 37 | //get all active by username 38 | [Authorize] 39 | [HttpGet("GetActiveByFirstName/{firstname}")] 40 | public IActionResult GetActiveByFirstName(string firstname) 41 | { 42 | var items = _userService.Get(a => a.IsActive && a.FirstName == firstname); 43 | return Ok(items); 44 | } 45 | 46 | //get one 47 | [Authorize] 48 | [HttpGet("{id}")] 49 | public IActionResult GetById(int id) 50 | { 51 | var item = _userService.GetOne(id); 52 | if (item == null) 53 | { 54 | Log.Error("GetById({ ID}) NOT FOUND", id); 55 | return NotFound(); 56 | } 57 | 58 | return Ok(item); 59 | } 60 | 61 | //add 62 | [Authorize(Roles = "Administrator")] 63 | [HttpPost] 64 | public IActionResult Create([FromBody] UserViewModel user) 65 | { 66 | if (user == null) 67 | return BadRequest(); 68 | 69 | var id = _userService.Add(user); 70 | return Created($"api/User/{id}", id); //HTTP201 Resource created 71 | } 72 | 73 | //update 74 | [Authorize(Roles = "Administrator")] 75 | [HttpPut("{id}")] 76 | public IActionResult Update(int id, [FromBody] UserViewModel user) 77 | { 78 | if (user == null || user.Id != id) 79 | return BadRequest(); 80 | 81 | int retVal = _userService.Update(user); 82 | if (retVal == 0) 83 | return StatusCode(304); //Not Modified 84 | else if (retVal == -1) 85 | return StatusCode(412, "DbUpdateConcurrencyException"); //412 Precondition Failed - concurrency 86 | else 87 | return Accepted(user); 88 | } 89 | 90 | //delete 91 | [Authorize(Roles = "Administrator")] 92 | [HttpDelete("{id}")] 93 | public IActionResult Delete(int id) 94 | { 95 | int retVal = _userService.Remove(id); 96 | if (retVal == 0) 97 | return NotFound(); //Not Found 404 98 | else if (retVal == -1) 99 | return StatusCode(412, "DbUpdateConcurrencyException"); //Precondition Failed - concurrency 100 | else 101 | return NoContent(); //No Content 204 102 | } 103 | 104 | } 105 | } 106 | 107 | 108 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/ExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Newtonsoft.Json; 3 | using Serilog; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Threading.Tasks; 9 | 10 | namespace SQLEFTableNotification.Api 11 | { 12 | /// 13 | /// Middleware - error handling 14 | /// 15 | public class ExceptionHandler 16 | { 17 | private readonly RequestDelegate _next; 18 | 19 | public ExceptionHandler(RequestDelegate next) 20 | { 21 | _next = next; 22 | } 23 | 24 | public async Task Invoke(HttpContext context) 25 | { 26 | try 27 | { 28 | await _next.Invoke(context); 29 | } 30 | catch (Exception ex) 31 | { 32 | await HandleExceptionAsync(context, ex); 33 | 34 | } 35 | } 36 | 37 | private async Task HandleExceptionAsync(HttpContext context, Exception exception) 38 | { 39 | var response = context.Response; 40 | response.ContentType = "application/json"; 41 | response.StatusCode = (int)HttpStatusCode.InternalServerError; 42 | var result = JsonConvert.SerializeObject(new 43 | { 44 | // customize as you need 45 | error = new 46 | { 47 | message = exception.Message, 48 | exception = exception.GetType().Name 49 | } 50 | }); 51 | await response.WriteAsync(result); 52 | //serilog 53 | Log.Error("ERROR FOUND", result); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Globals.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SQLEFTableNotification.Api 7 | { 8 | public static class Globals 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using Loggly; 2 | using Loggly.Config; 3 | using Microsoft.AspNetCore; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | using Serilog; 9 | using Serilog.Events; 10 | using SQLEFTableNotification.Api.Settings; 11 | using System; 12 | using System.IO; 13 | 14 | /// 15 | /// Designed by AnaSoft Inc. 2019 16 | /// http://www.anasoft.net/apincore 17 | /// 18 | /// Download full version from http://www.anasoft.net/apincore with these added features: 19 | /// -XUnit integration tests project (update the connection string and run tests) 20 | /// -API Postman tests as json file 21 | /// -JWT and IS4 authentication tests 22 | /// -T4 for smart code generation for new entities views, services, controllers and tests 23 | /// -demo videos https://www.youtube.com/channel/UC5XyWfG0nGYp7Q9buusealA 24 | /// 25 | /// Another VSIX control can be downloaded to create API .NET Core solution with Dapper ORM implemented instead of Entity Framework and for migration 26 | /// FluentMigrator.Runner is added to created solution. 27 | /// 28 | /// NOTE: 29 | /// Must update database connection in appsettings.json - "SQLEFTableNotification.ApiDB" 30 | /// 31 | 32 | namespace SQLEFTableNotification.Api 33 | { 34 | public class Program 35 | { 36 | 37 | private static string _environmentName; 38 | 39 | public static void Main(string[] args) 40 | { 41 | var webHost = CreateHostBuilder(args).Build(); 42 | 43 | //read configuration 44 | var configuration = new ConfigurationBuilder() 45 | .SetBasePath(Directory.GetCurrentDirectory()) 46 | .AddJsonFile("appsettings.json") 47 | .AddJsonFile($"appsettings.{_environmentName}.json", optional: true, reloadOnChange: true) 48 | .Build(); 49 | 50 | //Must have Loggly account and setup correct info in appsettings 51 | if (configuration["Serilog:UseLoggly"] == "true") 52 | { 53 | var logglySettings = new LogglySettings(); 54 | configuration.GetSection("Serilog:Loggly").Bind(logglySettings); 55 | SetupLogglyConfiguration(logglySettings); 56 | } 57 | 58 | Log.Logger = new LoggerConfiguration() 59 | .ReadFrom.Configuration(configuration) 60 | .CreateLogger(); 61 | 62 | //Start webHost 63 | try 64 | { 65 | Log.Information("Starting web host"); 66 | webHost.Run(); 67 | } 68 | catch (Exception ex) 69 | { 70 | Log.Fatal(ex, "Host terminated unexpectedly"); 71 | } 72 | finally 73 | { 74 | Log.CloseAndFlush(); 75 | } 76 | 77 | } 78 | 79 | 80 | public static IHostBuilder CreateHostBuilder(string[] args) => 81 | Host.CreateDefaultBuilder(args) 82 | .ConfigureLogging((hostingContext, config) => 83 | { 84 | config.ClearProviders(); //Disabling default integrated logger 85 | _environmentName = hostingContext.HostingEnvironment.EnvironmentName; 86 | }) 87 | .ConfigureWebHostDefaults(webBuilder => 88 | { 89 | webBuilder.UseStartup().UseSerilog(); 90 | }); 91 | 92 | 93 | /// 94 | /// Configure Loggly provider 95 | /// 96 | /// 97 | private static void SetupLogglyConfiguration(LogglySettings logglySettings) 98 | { 99 | //Configure Loggly 100 | var config = LogglyConfig.Instance; 101 | config.CustomerToken = logglySettings.CustomerToken; 102 | config.ApplicationName = logglySettings.ApplicationName; 103 | config.Transport = new TransportConfiguration() 104 | { 105 | EndpointHostname = logglySettings.EndpointHostname, 106 | EndpointPort = logglySettings.EndpointPort, 107 | LogTransport = logglySettings.LogTransport 108 | }; 109 | config.ThrowExceptions = logglySettings.ThrowExceptions; 110 | 111 | //Define Tags sent to Loggly 112 | config.TagConfig.Tags.AddRange(new ITag[]{ 113 | new ApplicationNameTag {Formatter = "Application-{0}"}, 114 | new HostnameTag { Formatter = "Host-{0}" } 115 | }); 116 | } 117 | 118 | 119 | } 120 | } 121 | 122 | 123 | //Configuration in code for serilog in Main 124 | //no appsetting option - in code settings not in configuration file 125 | //Log.Logger = new LoggerConfiguration() 126 | // .MinimumLevel.Debug() 127 | // .MinimumLevel.Override("Microsoft", LogEventLevel.Information) 128 | // .Enrich.FromLogContext() 129 | // .WriteTo.Console() 130 | // .WriteTo.File( 131 | // @"iCodify-API.txt", 132 | // fileSizeLimitBytes: 1_000_000, 133 | // rollOnFileSizeLimit: true, 134 | // shared: true, 135 | // flushToDiskInterval: TimeSpan.FromSeconds(1)) 136 | // .CreateLogger(); -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:44341/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "api/info", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "SQLEFTableNotificationApi": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "api/info", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | }, 26 | "applicationUrl": "http://localhost:44342/" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/SQLEFTableNotification.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | PreserveNewest 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Seed/accounts.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "Created": "1/1/2018", 3 | "Modified" :"1/1/2018", 4 | "Name": "Account 1", 5 | "Email": "apincore@anasoft.net", 6 | "Description": " ", 7 | "IsTrial": false, 8 | "IsActive": true, 9 | "SetActive": "1/1/2018" 10 | }] -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Seed/users.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "Created": "1/1/2018", 3 | "Modified": "1/1/2018", 4 | "FirstName": "TestF", 5 | "LastName": "TestF", 6 | "UserName": "my@email.com", 7 | "Email": "my@email.com", 8 | "Description": " ", 9 | "IsAdminRole": true, 10 | "Roles": "Administrator", 11 | "IsActive": true, 12 | "Password": "peaRlg4IIkOWvH2ZF0NjIJHr/CrykSgt5u6wsolkFNCwYoJJEPJ1fZryeBaxcpLZ", 13 | "AccountId": 1 14 | }] -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Settings/LogglySettings.cs: -------------------------------------------------------------------------------- 1 | using Loggly.Config; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace SQLEFTableNotification.Api.Settings 8 | { 9 | public class LogglySettings 10 | { 11 | public string ApplicationName { get; set; } 12 | public string Account { get; set; } 13 | public string Username { get; set; } 14 | public string Password { get; set; } 15 | public int EndpointPort { get; set; } 16 | public bool IsEnabled { get; set; } 17 | public bool ThrowExceptions { get; set; } 18 | public LogTransport LogTransport { get; set; } 19 | public string EndpointHostname { get; set; } 20 | public string CustomerToken { get; set; } 21 | } 22 | 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.AspNetCore.Authentication.JwtBearer; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Diagnostics; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 9 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 10 | using Microsoft.AspNetCore.Mvc.Versioning; 11 | using Microsoft.EntityFrameworkCore; 12 | using Microsoft.Extensions.Configuration; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Microsoft.Extensions.Logging; 15 | using Microsoft.Extensions.Options; 16 | using Microsoft.IdentityModel.Tokens; 17 | using Microsoft.OpenApi.Models; 18 | using Newtonsoft.Json.Serialization; 19 | using Serilog; 20 | using SQLEFTableNotification.Api; 21 | using SQLEFTableNotification.Domain.Mapping; 22 | using SQLEFTableNotification.Domain.Service; 23 | using SQLEFTableNotification.Entity.Context; 24 | using SQLEFTableNotification.Entity.Repository; 25 | using SQLEFTableNotification.Entity.UnitofWork; 26 | using System; 27 | using System.Collections.Generic; 28 | using System.Linq; 29 | using System.Net; 30 | using System.Text; 31 | using System.Threading.Tasks; 32 | 33 | /// 34 | /// Designed by AnaSoft Inc. 2019 35 | /// 36 | /// http://www.anasoft.net/apincore 37 | /// 38 | /// Download full version from http://www.anasoft.net/apincore with these added features: 39 | /// -XUnit integration tests project (update the connection string and run tests) 40 | /// -API Postman tests as json file 41 | /// -JWT and IS4 authentication tests 42 | /// -T4 for smart code generation based on new entities: domains, services, controllers and tests 43 | /// 44 | /// VSIX version with: 45 | /// -Dapper ORM implemented instead of Entity Framework and for migration 46 | /// -FluentMigrator.Runner 47 | /// 48 | /// NOTE: 49 | /// Must update database connection in appsettings.json - "SQLEFTableNotification.ApiDB" 50 | /// 51 | /// Select authentication type JWT or IS4 in appsettings.json; IS4 default 52 | /// Get client settings and tests for IS4 connectivity in http://www.anasoft.net/apincore 53 | /// 54 | 55 | namespace SQLEFTableNotification.Api 56 | { 57 | public class Startup 58 | { 59 | 60 | public static IConfiguration Configuration { get; set; } 61 | public IWebHostEnvironment HostingEnvironment { get; private set; } 62 | 63 | public Startup(IConfiguration configuration, IWebHostEnvironment env) 64 | { 65 | Configuration = configuration; 66 | HostingEnvironment = env; 67 | } 68 | 69 | public void ConfigureServices(IServiceCollection services) 70 | { 71 | 72 | Log.Information("Startup::ConfigureServices"); 73 | 74 | try 75 | { 76 | services.AddControllers( 77 | opt => 78 | { 79 | //Custom filters can be added here 80 | //opt.Filters.Add(typeof(CustomFilterAttribute)); 81 | //opt.Filters.Add(new ProducesAttribute("application/json")); 82 | } 83 | ).SetCompatibilityVersion(CompatibilityVersion.Version_3_0); 84 | 85 | #region "API versioning" 86 | //API versioning service 87 | services.AddApiVersioning( 88 | o => 89 | { 90 | //o.Conventions.Controller().HasApiVersion(1, 0); 91 | o.AssumeDefaultVersionWhenUnspecified = true; 92 | o.ReportApiVersions = true; 93 | o.DefaultApiVersion = new ApiVersion(1, 0); 94 | o.ApiVersionReader = new UrlSegmentApiVersionReader(); 95 | } 96 | ); 97 | 98 | // format code as "'v'major[.minor][-status]" 99 | services.AddVersionedApiExplorer( 100 | options => 101 | { 102 | options.GroupNameFormat = "'v'VVV"; 103 | //versioning by url segment 104 | options.SubstituteApiVersionInUrl = true; 105 | }); 106 | #endregion 107 | 108 | //db service 109 | if (Configuration["ConnectionStrings:UseInMemoryDatabase"] == "True") 110 | services.AddDbContext(opt => opt.UseInMemoryDatabase("TestDB-" + Guid.NewGuid().ToString())); 111 | else 112 | services.AddDbContext(options => options.UseSqlServer(Configuration["ConnectionStrings:SQLEFTableNotificationDB"])); 113 | 114 | #region "Authentication" 115 | //Authentication:IdentityServer4 - full version 116 | //JWT API authentication service 117 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 118 | .AddJwtBearer(options => 119 | { 120 | options.TokenValidationParameters = new TokenValidationParameters 121 | { 122 | ValidateIssuer = true, 123 | ValidateAudience = true, 124 | ValidateLifetime = true, 125 | ValidateIssuerSigningKey = true, 126 | ValidIssuer = Configuration["Jwt:Issuer"], 127 | ValidAudience = Configuration["Jwt:Issuer"], 128 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])) 129 | }; 130 | } 131 | ); 132 | #endregion 133 | 134 | #region "CORS" 135 | // include support for CORS 136 | // More often than not, we will want to specify that our API accepts requests coming from other origins (other domains). When issuing AJAX requests, browsers make preflights to check if a server accepts requests from the domain hosting the web app. If the response for these preflights don't contain at least the Access-Control-Allow-Origin header specifying that accepts requests from the original domain, browsers won't proceed with the real requests (to improve security). 137 | services.AddCors(options => 138 | { 139 | options.AddPolicy("CorsPolicy-public", 140 | builder => builder.AllowAnyOrigin() //WithOrigins and define a specific origin to be allowed (e.g. https://mydomain.com) 141 | .AllowAnyMethod() 142 | .AllowAnyHeader() 143 | //.AllowCredentials() 144 | .Build()); 145 | }); 146 | #endregion 147 | 148 | #region "MVC and JSON options" 149 | //mvc service (set to ignore ReferenceLoopHandling in json serialization like Users[0].Account.Users) 150 | //in case you need to serialize entity children use commented out option instead 151 | services.AddMvc(option => option.EnableEndpointRouting = false) 152 | .AddNewtonsoftJson(options => { options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; }); //NO entity classes' children serialization 153 | //.AddNewtonsoftJson(ops => 154 | //{ 155 | // ops.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize; 156 | // ops.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; 157 | //}); //WITH entity classes' children serialization 158 | #endregion 159 | 160 | #region "DI code" 161 | //general unitofwork injections 162 | services.AddTransient(); 163 | 164 | //services injections 165 | services.AddTransient(typeof(AccountService<,>), typeof(AccountService<,>)); 166 | services.AddTransient(typeof(UserService<,>), typeof(UserService<,>)); 167 | services.AddTransient(typeof(AccountServiceAsync<,>), typeof(AccountServiceAsync<,>)); 168 | services.AddTransient(typeof(UserServiceAsync<,>), typeof(UserServiceAsync<,>)); 169 | //...add other services 170 | // 171 | services.AddTransient(typeof(IService<,>), typeof(GenericService<,>)); 172 | services.AddTransient(typeof(IServiceAsync<,>), typeof(GenericServiceAsync<,>)); 173 | #endregion 174 | 175 | //data mapper services configuration 176 | services.AddAutoMapper(typeof(MappingProfile)); 177 | 178 | 179 | } 180 | catch (Exception ex) 181 | { 182 | Log.Error(ex.Message); 183 | } 184 | } 185 | 186 | 187 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 188 | { 189 | 190 | Log.Information("Startup::Configure"); 191 | 192 | try 193 | { 194 | if (env.EnvironmentName == "Development") 195 | app.UseDeveloperExceptionPage(); 196 | else 197 | app.UseMiddleware(); 198 | 199 | app.UseCors("CorsPolicy-public"); //apply to every request 200 | app.UseAuthentication(); //needs to be up in the pipeline, before MVC 201 | app.UseAuthorization(); 202 | 203 | app.UseMvc(); 204 | 205 | //migrations and seeds from json files 206 | using (var serviceScope = app.ApplicationServices.GetRequiredService().CreateScope()) 207 | { 208 | if (Configuration["ConnectionStrings:UseInMemoryDatabase"] == "False" && !serviceScope.ServiceProvider.GetService().AllMigrationsApplied()) 209 | { 210 | if (Configuration["ConnectionStrings:UseMigrationService"] == "True") 211 | serviceScope.ServiceProvider.GetService().Database.Migrate(); 212 | } 213 | //it will seed tables on aservice run from json files if tables empty 214 | if (Configuration["ConnectionStrings:UseSeedService"] == "True") 215 | serviceScope.ServiceProvider.GetService().EnsureSeeded(); 216 | } 217 | } 218 | catch (Exception ex) 219 | { 220 | Log.Error(ex.Message); 221 | } 222 | 223 | } 224 | } 225 | } 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/_nugets.txt: -------------------------------------------------------------------------------- 1 | // 2 | //NOTE: All packages will be installed automatically by VS with proper versions 3 | // 4 | 5 | More info: https://www.anasoft.net/apincore 6 | ______________________________________________________________ 7 | 8 | dotnet restore 9 | 10 | https://www.nuget.org/packages/Microsoft.AspNetCore 11 | install-Package Microsoft.AspNetCore 12 | 13 | https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc/ 14 | PM> Install-Package Microsoft.AspNetCore.Mvc 15 | 16 | https://www.nuget.org/packages/automapper/ 17 | PM> Install-Package AutoMapper 18 | 19 | https://www.newtonsoft.com/json 20 | PM> Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson 21 | //required when use this db provider 22 | Install-Package Microsoft.EntityFrameworkCore.InMemory 23 | 24 | PM> Install-Package Serilog.AspNetCore -DependencyVersion Highest 25 | PM> Install-Package Serilog.Sinks.Console 26 | PM> Install-Package Serilog.Sinks.File 27 | PM> Install-Package Serilog.Sinks.Loggly 28 | PM> Install-Package Serilog.Settings.Configuration 29 | 30 | PM> Install-Package Swashbuckle.AspNetCore 31 | 32 | ------------- 33 | Serilog Logger 34 | https://github.com/serilog/serilog-aspnetcore 35 | https://itnext.io/loggly-in-asp-net-core-using-serilog-dc0e2c7d52eb 36 | https://www.blinkingcaret.com/2018/02/14/net-core-console-logging/ 37 | https://nblumhardt.com/2017/08/use-serilog/ 38 | https://github.com/loggly/log4net-loggly/#net-core-support 39 | ------------- 40 | 41 | PM> Install-Package IdentityServer4.AccessTokenValidation 42 | 43 | 44 | -www.anasoft.net/apincore 45 | 46 | 47 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/_readme.txt: -------------------------------------------------------------------------------- 1 | 2 | https://www.anasoft.net/restapi 3 | https://studio.youtube.com/channel/UC5XyWfG0nGYp7Q9buusealA/videos 4 | 5 | 6 | RestApiNEx/ApiNCoreEx is Visual Studio extension for building .NET(5/6/7+) and .NET Core REST API services. 7 | Generate 100s lines of code on a single click and save hours of searching Web and testing for the right REST API solution. 8 | A great start for creating REST API service based on latest .NET(5/6/7+) and .NET Core. These extensions work with all Visual Studio editions including free Community VS edition. 9 | 10 | **************** 11 | Get all these extensions: 12 | **************** 13 | RestApiNEx(.NET Core 5/6/7+)(.NET Core 2.2 and 3.1) extensions with EF ORM and T4s Postman test, Swagger, DDoS attacks protection, LazyCache, BLAZOR app test client, and T4s code generator for all solution projects, 14 | RestApiNLx(.NET 5/6/7+) extension with simple REST API implementation , 15 | RestApiNDx (.NET Core 5/6/7/+) extensions with Dapper ORM, FluentMigrator, Postman, Swagger, DDoS attacks protection and T4s code generator, 16 | RestApiPyEx Python REST API vsix extension (+wrapper) with flask, json, pyodbc, flasgger, swagger and db migration 17 | BONUS: Google Drive JQuery plugin demo solution (quickly connect and use GDrive storage from JavaScript) 18 | 19 | To get full-featured VSIX extensions visit: https://www.anasoft.net/restapi 20 | 21 | with detailed video instructions how to build full-featured REST API in no time. 22 | 23 | 24 | **************** 25 | Reviews 26 | **************** 27 | S.M. 2022-02-16 28 | Hello guys, i bought your perfect REST API VS Template... 29 | D.R. 2021-11-29 30 | Thank you and congratulate you for an awesome plugin and project, it seriously simplifies things! 31 | G. B. 2021-11-08 32 | Thanks for your nice API template, it's frankly a very good job. 33 | I would recommend it, and I remain attentive to your future creations. 34 | J.M. 2021-03-09 35 | Saves you a lot of time. 36 | M.D. 2020-10-02 37 | Thanks for the good work. Your template was a perfect introduction into the API world. 38 | G.B. 2020-09-16 39 | A great product that has really saved me hours and hours of work. Really looking forward to the IaaS support coming for AWS. 40 | D.T. 2020-03-30 41 | Well worth the money even if 5 times the price - I would have paid more. Excellent videos which you great rarely even with paid for products - I can think of one that cost £80k/pa that has less videos and more bugs. Some suppliers think they are gods gift and you they pick you - they just highly paid sales men. Sure this product is just a good bit of well architected code driven by T4 - but just imagine how much time it saves you? Days? and still it is only a few dollars not $2500 Better get in there before they charge what the market can stand not how much time it too to develop. 42 | F. 2020-02-28 43 | Sangat membantu sekali. Dengan sekali Clik, bisa menghemat waktu banyak, dengan minim kesalahan... 44 | H.G. 2019-12-30 45 | A great saving time template!! 46 | J.R. 2019-11-13 47 | This template saves me a lot of time in creating new API's with all the plumbing necessary for an API to run efficiently. 48 | P.W. 2019-07-10 49 | This is an excellent template. It makes scaffolding a .NET Core API project painless and saves a lot of time. 50 | 51 | ***************** 52 | 53 | Thank you. 54 | code@Anasoft.net team -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "UseMigrationService": true, 4 | "UseSeedService": true, 5 | "UseInMemoryDatabase": false, 6 | "SQLEFTableNotificationDB": "Data Source=.\\SQLEXPRESS;Initial Catalog=SQLEFTableNotification;Trusted_Connection=True;MultipleActiveResultSets=True;" 7 | }, 8 | //"Logging": { 9 | // "IncludeScopes": false, 10 | // "Debug": { 11 | // "LogLevel": { 12 | // "Default": "Warning" 13 | // } 14 | // }, 15 | // "Console": { 16 | // "LogLevel": { 17 | // "Default": "Debug" 18 | // } 19 | // } 20 | //}, 21 | "Serilog": { 22 | "MinimumLevel": "Debug", 23 | "WriteTo": [ 24 | { 25 | "Name": "Console", 26 | "Args": { 27 | "outputTemplate": "===> {Timestamp:HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}" 28 | } 29 | }, 30 | { 31 | "Name": "RollingFile", 32 | "Args": { 33 | "pathFormat": "logs/SQLEFTableNotification-API-{Date}.txt", 34 | "outputTemplate": "===> {Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}" 35 | } 36 | }, 37 | { 38 | "Name": "Loggly" 39 | } 40 | ], 41 | "UseLoggly": false, 42 | "Loggly": 43 | { 44 | "ApplicationName": "SQLEFTableNotification", 45 | "Account": "yourLogglyAccount", 46 | "Username": "YourLogglyUserName", 47 | //"Password": "lalala", 48 | //"EndpointPort": "443", 49 | "IsEnabled": "true", 50 | "ThrowExceptions": "true", 51 | "LogTransport": "Https", 52 | "EndpointHostname": "logs-01.loggly.com", 53 | "CustomerToken": "1aa11a1a1-aa11-aa11-a11a-1a1aaa111a1a" //Loggly account customer token 54 | } 55 | }, 56 | "Jwt": { 57 | "Key": "12345678910111213141516", 58 | "Issuer": "http://localhost:44342/" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/BaseDomain.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace SQLEFTableNotification.Domain 6 | { 7 | public class BaseDomain 8 | { 9 | public int Id { get; set; } 10 | public byte[] RowVersion { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Domain/2_t4DomainViewModelsGenerate.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ output extension=".cs" #> 3 | <#@ assembly name="EnvDTE" #> 4 | <#@ assembly name="Microsoft.VisualStudio.OLE.Interop" #> 5 | <#@ assembly name="Microsoft.VisualStudio.Shell" #> 6 | <#@ assembly name="Microsoft.VisualStudio.Shell.Interop" #> 7 | <#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #> 8 | <#@ import namespace="System.Collections.Generic" #> 9 | <#@ import namespace="System.Diagnostics" #> 10 | <#@ import namespace="System.IO" #> 11 | <#@ import namespace="System.Reflection"#> 12 | 13 | 14 | <# 15 | //For full version with complete T4 features go to http://www.anasoft.net/apincore 16 | 17 | //var tc = new TemplateCommon(); 18 | //get list of entity classes to use as templates to create domain clases - For full version with complete T4 features go to http://www.anasoft.net/apincore 19 | //List entityClassesNotExistsinDomain = tc.GetClassesToCreate(false,"SQLEFTableNotification.Entity", "BaseEntity","SQLEFTableNotification.Domain", "BaseDomain","ViewModel"); 20 | 21 | List entityClassesNotExistsinDomain = new List{"Entity1","Entity2"}; 22 | List props = new List{"prop1","prop2"}; 23 | 24 | #> 25 | // —————————————— 26 | // 27 | // 28 | // For full version with complete T4 features go to http://www.anasoft.net/apincore 29 | // 30 | // This code was auto-generated <#= DateTime.Now #> 31 | // NOTE:T4 generated code may need additional updates/addjustments by developer in order to compile a solution. 32 | // 33 | // —————————————– 34 | using System; 35 | using System.Collections.Generic; 36 | namespace SQLEFTableNotification.Domain 37 | { 38 | <# 39 | //create domain classes 40 | foreach(string cl in entityClassesNotExistsinDomain) 41 | { 42 | var entityName = cl; 43 | #> 44 | 45 | /// 46 | /// A <#= entityName #> view model 47 | /// 48 | public class <#= entityName #>ViewModel : BaseDomain 49 | { 50 | <# 51 | //var props = tc.GetAllProperties(cl); 52 | foreach(string prop in props) 53 | { 54 | var propn = prop; 55 | //EnvDTE.CodeTypeRef codeTypeRef = prop.Type; 56 | //var propt = codeTypeRef.AsString; 57 | var propt = "string"; 58 | #> 59 | public <#= propt #> <#= propn #> { get; set; } 60 | <# 61 | } 62 | #> 63 | } 64 | <# 65 | } 66 | #> 67 | 68 | } -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Domain/AccountViewModel.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace SQLEFTableNotification.Domain 6 | { 7 | /// 8 | /// A account with users 9 | /// 10 | public class AccountViewModel : BaseDomain 11 | { 12 | public string Name { get; set; } 13 | public string Email { get; set; } 14 | public string Description { get; set; } 15 | public bool IsTrial { get; set; } 16 | public bool IsActive { get; set; } 17 | public DateTime SetActive { get; set; } 18 | 19 | public virtual ICollection Users { get; set; } 20 | } 21 | 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Domain/UserViewModel.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Text; 6 | 7 | namespace SQLEFTableNotification.Domain 8 | { 9 | /// 10 | /// A user attached to an account 11 | /// 12 | public class UserViewModel : BaseDomain 13 | { 14 | public string FirstName { get; set; } 15 | public string LastName { get; set; } 16 | public string UserName { get; set; } 17 | public string Email { get; set; } 18 | public string Description { get; set; } 19 | public bool IsAdminRole { get; set; } 20 | public ICollection Roles { get; set; } //map from semicolumn delimited from Entity 21 | public bool IsActive { get; set; } 22 | public string Password { get; set; } 23 | public int AccountId { get; set; } 24 | 25 | [JsonIgnore] //to avoid circular serialization 26 | public virtual AccountViewModel Account { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Mapping/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using SQLEFTableNotification.Entity; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace SQLEFTableNotification.Domain.Mapping 8 | { 9 | public class MappingProfile : Profile 10 | { 11 | /// 12 | /// Create automap mapping profiles 13 | /// 14 | public MappingProfile() 15 | { 16 | CreateMap(); 17 | CreateMap(); 18 | CreateMap() 19 | .ForMember(dest => dest.DecryptedPassword, opts => opts.MapFrom(src => src.Password)) 20 | .ForMember(dest => dest.Roles, opts => opts.MapFrom(src => string.Join(";", src.Roles))); 21 | CreateMap() 22 | .ForMember(dest => dest.Password, opts => opts.MapFrom(src => src.DecryptedPassword)) 23 | .ForMember(dest => dest.Roles, opts => opts.MapFrom(src => src.Roles.Split(";", StringSplitOptions.RemoveEmptyEntries))); 24 | 25 | } 26 | 27 | } 28 | 29 | 30 | 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/SQLEFTableNotification.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | TextTemplatingFileGenerator 20 | Domain\2_t4DomainViewModelsGenerate.cs 21 | 22 | 23 | 24 | 25 | 26 | TextTemplatingFileGenerator 27 | Service\4_t4DomainServicesGenerate.cs 28 | 29 | 30 | 31 | 32 | 33 | True 34 | True 35 | Domain\2_t4DomainViewModelsGenerate.tt 36 | 37 | 38 | 39 | 40 | 41 | True 42 | True 43 | Service\4_t4DomainServicesGenerate.tt 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Service/4_t4DomainServicesGenerate.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ output extension=".cs" #> 3 | <#@ assembly name="EnvDTE" #> 4 | <#@ assembly name="Microsoft.VisualStudio.OLE.Interop" #> 5 | <#@ assembly name="Microsoft.VisualStudio.Shell" #> 6 | <#@ assembly name="Microsoft.VisualStudio.Shell.Interop" #> 7 | <#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #> 8 | <#@ import namespace="System.Collections.Generic" #> 9 | <#@ import namespace="System.Diagnostics" #> 10 | <#@ import namespace="System.IO" #> 11 | <#@ import namespace="System.Reflection"#> 12 | 13 | 14 | <# 15 | //For full version with complete T4 features go to http://www.anasoft.net/apincore 16 | 17 | //var tc = new TemplateCommon(); 18 | //get list of entity classes to use as templates to create domain clases - For full version with complete T4 features go to http://www.anasoft.net/apincore 19 | //List entityClassesNotExistsinService = tc.GetClassesToCreate(false,"SQLEFTableNotification.Entity", "BaseEntity","SQLEFTableNotification.Domain", "GenericService","Service"); 20 | 21 | List entityClassesNotExistsinService = new List{"Entity1","Entity2"}; 22 | #> 23 | // —————————————— 24 | // 25 | // 26 | // For full version with complete T4 features go to http://www.anasoft.net/apincore 27 | // 28 | // This code was auto-generated <#= DateTime.Now #> 29 | // T4 template produces service class code if a class does not exist 30 | // NOTE:T4 generated code may need additional updates/addjustments by developer in order to compile a solution. 31 | // 32 | // —————————————– 33 | using System; 34 | using System.Collections.Generic; 35 | using System.Linq.Expressions; 36 | using System.Text; 37 | using AutoMapper; 38 | using SQLEFTableNotification.Entity; 39 | using SQLEFTableNotification.Entity.UnitofWork; 40 | 41 | namespace SQLEFTableNotification.Domain.Service 42 | { 43 | <# 44 | foreach(string cl in entityClassesNotExistsinService) 45 | { 46 | var entityName = cl; 47 | #> 48 | /// 49 | /// A <#= entityName #> service 50 | /// 51 | 52 | <# 53 | } 54 | #> 55 | } -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Service/AccountService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using SQLEFTableNotification.Entity; 3 | using SQLEFTableNotification.Entity.UnitofWork; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq.Expressions; 7 | using System.Text; 8 | 9 | namespace SQLEFTableNotification.Domain.Service 10 | { 11 | public class AccountService : GenericService 12 | where Tv : AccountViewModel 13 | where Te : Account 14 | { 15 | //DI must be implemented in specific service as well beside GenericService constructor 16 | public AccountService(IUnitOfWork unitOfWork, IMapper mapper) 17 | { 18 | if (_unitOfWork == null) 19 | _unitOfWork = unitOfWork; 20 | if (_mapper == null) 21 | _mapper = mapper; 22 | } 23 | 24 | //add here any custom service method or override generic service method 25 | public bool DoNothing() 26 | { 27 | return true; 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Service/AccountServiceAsync.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using SQLEFTableNotification.Entity; 3 | using SQLEFTableNotification.Entity.UnitofWork; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq.Expressions; 7 | using System.Text; 8 | 9 | namespace SQLEFTableNotification.Domain.Service 10 | { 11 | public class AccountServiceAsync : GenericServiceAsync 12 | where Tv : AccountViewModel 13 | where Te : Account 14 | { 15 | //DI must be implemented specific service as well beside GenericAsyncService constructor 16 | public AccountServiceAsync(IUnitOfWork unitOfWork, IMapper mapper) 17 | { 18 | if (_unitOfWork == null) 19 | _unitOfWork = unitOfWork; 20 | if (_mapper == null) 21 | _mapper = mapper; 22 | } 23 | 24 | //add here any custom service method or override genericasync service method 25 | //... 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Service/Generic/GenericService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using SQLEFTableNotification.Entity; 3 | using SQLEFTableNotification.Entity.UnitofWork; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq.Expressions; 7 | using System.Text; 8 | 9 | namespace SQLEFTableNotification.Domain.Service 10 | { 11 | public class GenericService : IService where Tv : BaseDomain 12 | where Te : BaseEntity 13 | { 14 | 15 | protected IUnitOfWork _unitOfWork; 16 | protected IMapper _mapper; 17 | public GenericService(IUnitOfWork unitOfWork, IMapper mapper) 18 | { 19 | _unitOfWork = unitOfWork; 20 | _mapper = mapper; 21 | } 22 | 23 | public GenericService() 24 | { 25 | } 26 | 27 | public virtual IEnumerable GetAll() 28 | { 29 | var entities = _unitOfWork.GetRepository() 30 | .GetAll(); 31 | return _mapper.Map>(source: entities); 32 | } 33 | public virtual Tv GetOne(int id) 34 | { 35 | var entity = _unitOfWork.GetRepository() 36 | .GetOne(predicate: x => x.Id == id); 37 | return _mapper.Map(source: entity); 38 | } 39 | 40 | public virtual int Add(Tv view) 41 | { 42 | var entity = _mapper.Map(source: view); 43 | _unitOfWork.GetRepository().Insert(entity); 44 | _unitOfWork.Save(); 45 | return entity.Id; 46 | } 47 | 48 | public virtual int Update(Tv view) 49 | { 50 | _unitOfWork.GetRepository().Update(view.Id, _mapper.Map(source: view)); 51 | return _unitOfWork.Save(); 52 | } 53 | 54 | 55 | public virtual int Remove(int id) 56 | { 57 | Te entity = _unitOfWork.Context.Set().Find(id); 58 | _unitOfWork.GetRepository().Delete(entity); 59 | return _unitOfWork.Save(); 60 | } 61 | 62 | public virtual IEnumerable Get(Expression> predicate) 63 | { 64 | var entities = _unitOfWork.GetRepository() 65 | .Get(predicate: predicate); 66 | return _mapper.Map>(source: entities); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Service/Generic/GenericServiceAsync.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using SQLEFTableNotification.Entity; 3 | using SQLEFTableNotification.Entity.UnitofWork; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq.Expressions; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | 11 | namespace SQLEFTableNotification.Domain.Service 12 | { 13 | public class GenericServiceAsync : IServiceAsync where Tv : BaseDomain 14 | where Te : BaseEntity 15 | { 16 | protected IUnitOfWork _unitOfWork; 17 | protected IMapper _mapper; 18 | public GenericServiceAsync(IUnitOfWork unitOfWork, IMapper mapper) 19 | { 20 | _unitOfWork = unitOfWork; 21 | _mapper = mapper; 22 | } 23 | 24 | public GenericServiceAsync() 25 | { 26 | } 27 | 28 | public virtual async Task> GetAll() 29 | { 30 | var entities = await _unitOfWork.GetRepositoryAsync() 31 | .GetAll(); 32 | return _mapper.Map>(source: entities); 33 | } 34 | 35 | public virtual async Task GetOne(int id) 36 | { 37 | var entity = await _unitOfWork.GetRepositoryAsync() 38 | .GetOne(predicate: x => x.Id == id); 39 | return _mapper.Map(source: entity); 40 | } 41 | 42 | public virtual async Task Add(Tv view) 43 | { 44 | var entity = _mapper.Map(source: view); 45 | await _unitOfWork.GetRepositoryAsync().Insert(entity); 46 | await _unitOfWork.SaveAsync(); 47 | return entity.Id; 48 | } 49 | 50 | public async Task Update(Tv view) 51 | { 52 | await _unitOfWork.GetRepositoryAsync().Update(view.Id, _mapper.Map(source: view)); 53 | return await _unitOfWork.SaveAsync(); 54 | } 55 | 56 | public virtual async Task Remove(int id) 57 | { 58 | Te entity = await _unitOfWork.Context.Set().FindAsync(id); 59 | await _unitOfWork.GetRepositoryAsync().Delete(id); 60 | return await _unitOfWork.SaveAsync(); 61 | } 62 | 63 | public virtual async Task> Get(Expression> predicate) 64 | { 65 | var items = await _unitOfWork.GetRepositoryAsync() 66 | .Get(predicate: predicate); 67 | return _mapper.Map>(source: items); 68 | } 69 | } 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Service/Generic/IService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Text; 5 | 6 | namespace SQLEFTableNotification.Domain.Service 7 | { 8 | public interface IService 9 | { 10 | IEnumerable GetAll(); 11 | int Add(Tv obj); 12 | int Update(Tv obj); 13 | int Remove(int id); 14 | Tv GetOne(int id); 15 | IEnumerable Get(Expression> predicate); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Service/Generic/IServiceAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SQLEFTableNotification.Domain.Service 8 | { 9 | public interface IServiceAsync 10 | { 11 | Task> GetAll(); 12 | Task Add(Tv obj); 13 | Task Update(Tv obj); 14 | Task Remove(int id); 15 | Task GetOne(int id); 16 | Task> Get(Expression> predicate); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Service/UserService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using SQLEFTableNotification.Entity; 3 | using SQLEFTableNotification.Entity.UnitofWork; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq.Expressions; 7 | using System.Text; 8 | 9 | namespace SQLEFTableNotification.Domain.Service 10 | { 11 | public class UserService : GenericService,IUserService 12 | where Tv : UserViewModel 13 | where Te : User 14 | { 15 | //DI must be implemented in specific service as well beside GenericService constructor 16 | public UserService(IUnitOfWork unitOfWork, IMapper mapper) 17 | { 18 | if (_unitOfWork == null) 19 | _unitOfWork = unitOfWork; 20 | if (_mapper == null) 21 | _mapper = mapper; 22 | } 23 | 24 | //add here any custom service method or override generic service method 25 | public bool DoNothing() 26 | { 27 | return true; 28 | } 29 | } 30 | 31 | internal interface IUserService: IService 32 | { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/Service/UserServiceAsync.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using SQLEFTableNotification.Entity; 3 | using SQLEFTableNotification.Entity.UnitofWork; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq.Expressions; 7 | using System.Text; 8 | 9 | namespace SQLEFTableNotification.Domain.Service 10 | { 11 | public class UserServiceAsync : GenericServiceAsync 12 | where Tv : UserViewModel 13 | where Te : User 14 | { 15 | //DI must be implemented specific service as well beside GenericAsyncService constructor 16 | public UserServiceAsync(IUnitOfWork unitOfWork, IMapper mapper) 17 | { 18 | if (_unitOfWork == null) 19 | _unitOfWork = unitOfWork; 20 | if (_mapper == null) 21 | _mapper = mapper; 22 | } 23 | 24 | //add here any custom service method or override genericasync service method 25 | //... 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Domain/_nugets.txt: -------------------------------------------------------------------------------- 1 | // 2 | //NOTE: All packages will be installed automatically by VS with proper versions 3 | // 4 | 5 | https://www.nuget.org/packages/automapper/ 6 | PM> Install-Package AutoMapper 7 | 8 | 9 | /// Designed by AnaSoft Inc. 2019-2020 10 | /// http://www.anasoft.net/apincore 11 | /// 12 | /// Download full version from http://www.anasoft.net/apincore with these added features: 13 | /// -XUnit integration tests project (update the connection string and run tests) 14 | /// -API Postman tests as json file 15 | /// -JWT and IS4 authentication tests 16 | /// -T4 for smart code generation for new entities views, services, controllers and tests 17 | 18 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/1_t4EntityHelpersGenerate.tt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jatinrdave/SQLEFTableNotification/3750f8b5c727f90792cfa4774a19fb42ce444ee5/SQLEFTableNotification/SQLEFTableNotification.Entity/1_t4EntityHelpersGenerate.tt -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace SQLEFTableNotification.Entity 6 | { 7 | public class BaseEntity:IEntityPk 8 | { 9 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 10 | [Key] 11 | public int Id { get; set; } 12 | public DateTime Created { get; set; } 13 | public DateTime Modified { get; set; } 14 | //[ConcurrencyCheck] 15 | //[Timestamp] 16 | public byte[] RowVersion { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/CodeGeneratorUtility-readme.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jatinrdave/SQLEFTableNotification/3750f8b5c727f90792cfa4774a19fb42ce444ee5/SQLEFTableNotification/SQLEFTableNotification.Entity/CodeGeneratorUtility-readme.pdf -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/CodeGeneratorUtility.bat: -------------------------------------------------------------------------------- 1 | :: 2 | :: 3 | echo off 4 | echo........................................ 5 | echo Generate solution code from added Entity classes 6 | echo........................................ 7 | echo off 8 | :PROMPT 9 | SET /P AREYOUSURE=Are you sure you want to delete generated files(Y/[N])? 10 | IF /I "%AREYOUSURE%" NEQ "Y" GOTO END 11 | 12 | ::Select the VS version 13 | SET tt="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\TextTransform.exe" 14 | ::SET tt="C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\Common7\IDE\TextTransform.exe" 15 | ::SET tt="C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\TextTransform.exe" 16 | ::SET tt="C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\TextTransform.exe" 17 | ::SET tt="C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\TextTransform.exe" 18 | ::SET tt="C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\TextTransform.exe" 19 | 20 | echo off 21 | echo Delete previously generated cs code files 22 | DEL /F "1_t4EntityHelpersGenerate.cs" 23 | DEL /F "..\SQLEFTableNotification.Domain\Domain\2_t4DomainViewModelsGenerate.cs" 24 | ::DEL /F "..\SQLEFTableNotification.Domain\Mapping\3_t4DomainMappingProfileGenerate.cs" 25 | DEL /F "..\SQLEFTableNotification.Domain\Service\4_t4DomainServicesGenerate.cs" 26 | ::DEL /F "..\SQLEFTableNotification.Api\Controllers\5_t4ApiControllerGenerate.cs" 27 | ::DEL /F "..\SQLEFTableNotification.Test\6_t4IntegrationTestGenerate.cs" 28 | echo . 29 | echo Run all T4s... 30 | echo -generate entity helpers 31 | %tt% "1_t4EntityHelpersGenerate.tt" -out "1_t4EntityHelpersGenerate.cs" 32 | echo -generate domain classes 33 | %tt% "..\SQLEFTableNotification.Domain\Domain\2_t4DomainViewModelsGenerate.tt" -out "..\SQLEFTableNotification.Domain\Domain\2_t4DomainViewModelsGenerate.cs" 34 | echo -generate mapper classes 35 | ::%tt% "..\SQLEFTableNotification.Domain\Mapping\3_t4DomainMappingProfileGenerate.tt" -out "..\SQLEFTableNotification.Domain\Mapping\3_t4DomainMappingProfileGenerate.cs" 36 | echo -generate services classes 37 | %tt% "..\SQLEFTableNotification.Domain\Service\4_t4DomainServicesGenerate.tt" -out "..\SQLEFTableNotification.Domain\Service\4_t4DomainServicesGenerate.cs" 38 | echo -generate controller classes 39 | ::%tt% "..\SQLEFTableNotification.Api\Controllers\5_t4ApiControllerGenerate.tt" -out "..\SQLEFTableNotification.Api\Controllers\5_t4ApiControllerGenerate.cs" 40 | echo -generate test classes 41 | ::%tt% "..\SQLEFTableNotification.Test\6_t4IntegrationTestGenerate.tt" -out "..\SQLEFTableNotification.Test\6_t4IntegrationTestGenerate.cs" 42 | echo T4s completed. 43 | pause 44 | :END -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Context/DBContextExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Infrastructure; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | using Newtonsoft.Json; 5 | using SQLEFTableNotification.Entity.Context; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Text; 11 | 12 | namespace SQLEFTableNotification.Entity.Context 13 | { 14 | public static class DbContextExtension 15 | { 16 | 17 | public static bool AllMigrationsApplied(this DbContext context) 18 | { 19 | var applied = context.GetService() 20 | .GetAppliedMigrations() 21 | .Select(m => m.MigrationId); 22 | 23 | var total = context.GetService() 24 | .Migrations 25 | .Select(m => m.Key); 26 | 27 | return !total.Except(applied).Any(); 28 | //return false; 29 | } 30 | 31 | public static void EnsureSeeded(this SQLEFTableNotificationContext context) 32 | { 33 | 34 | if (!context.Accounts.Any()) 35 | { 36 | var accounts = JsonConvert.DeserializeObject>(File.ReadAllText("seed" + Path.DirectorySeparatorChar + "accounts.json")); 37 | context.AddRange(accounts); 38 | context.SaveChanges(); 39 | } 40 | 41 | //Ensure we have some status 42 | if (!context.Users.Any()) 43 | { 44 | var users = JsonConvert.DeserializeObject>(File.ReadAllText(@"seed" + Path.DirectorySeparatorChar + "users.json")); 45 | context.AddRange(users); 46 | context.SaveChanges(); 47 | } 48 | } 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Context/SQLEFTableNotificationContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Configuration; 3 | using System; 4 | using System.Configuration; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace SQLEFTableNotification.Entity.Context 9 | { 10 | public partial class SQLEFTableNotificationContext : DbContext 11 | { 12 | 13 | public SQLEFTableNotificationContext(DbContextOptions options) 14 | : base(options) 15 | { 16 | } 17 | 18 | public DbSet Accounts { get; set; } 19 | public DbSet Users { get; set; } 20 | 21 | //lazy-loading 22 | //https://entityframeworkcore.com/querying-data-loading-eager-lazy 23 | //https://docs.microsoft.com/en-us/ef/core/querying/related-data 24 | //EF Core will enable lazy-loading for any navigation property that is virtual and in an entity class that can be inherited 25 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 26 | => optionsBuilder 27 | .UseLazyLoadingProxies(); 28 | 29 | protected override void OnModelCreating(ModelBuilder modelBuilder) 30 | { 31 | //Fluent API 32 | modelBuilder.Entity() 33 | .HasOne(u => u.Account) 34 | .WithMany(e => e.Users); 35 | 36 | //concurrency 37 | modelBuilder.Entity() 38 | .Property(a => a.RowVersion).IsRowVersion(); 39 | modelBuilder.Entity() 40 | .Property(a => a.RowVersion).IsRowVersion(); 41 | 42 | //modelBuilder.Entity() 43 | //.Property(p => p.DecryptedPassword) 44 | //.HasComputedColumnSql("Uncrypt(p.PasswordText)"); 45 | } 46 | 47 | public override int SaveChanges() 48 | { 49 | Audit(); 50 | return base.SaveChanges(); 51 | } 52 | 53 | public async Task SaveChangesAsync() 54 | { 55 | Audit(); 56 | return await base.SaveChangesAsync(); 57 | } 58 | 59 | private void Audit() 60 | { 61 | var entries = ChangeTracker.Entries().Where(x => x.Entity is BaseEntity && (x.State == EntityState.Added || x.State == EntityState.Modified)); 62 | foreach (var entry in entries) 63 | { 64 | if (entry.State == EntityState.Added) 65 | { 66 | ((BaseEntity)entry.Entity).Created = DateTime.UtcNow; 67 | } 68 | ((BaseEntity)entry.Entity).Modified = DateTime.UtcNow; 69 | } 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Entity/Account.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Text; 5 | 6 | namespace SQLEFTableNotification.Entity 7 | { 8 | /// 9 | /// A account with users 10 | /// 11 | public class Account : BaseEntity 12 | { 13 | [Required] 14 | [StringLength(30)] 15 | public string Name { get; set; } 16 | [Required] 17 | [StringLength(30)] 18 | public string Email { get; set; } 19 | [StringLength(255)] 20 | public string Description { get; set; } 21 | public bool IsTrial { get; set; } 22 | public bool IsActive { get; set; } 23 | public DateTime SetActive { get; set; } 24 | 25 | public virtual ICollection Users { get; set; } 26 | 27 | } 28 | 29 | 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Entity/ChangeTable.cs: -------------------------------------------------------------------------------- 1 | using SQLEFTableNotification.Entity.Model; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace SQLEFTableNotification.Entity.Entity 10 | { 11 | public class ChangeTable 12 | { 13 | public long? SYS_CHANGE_VERSION { get; set; } 14 | public long? SYS_CHANGE_CREATION_VERSION { get; set; } 15 | public string SYS_CHANGE_OPERATION { get; set; } 16 | public byte[] SYS_CHANGE_COLUMNS { get; set; } 17 | public byte[] SYS_CHANGE_CONTEXT { get; set; } 18 | [NotMapped] 19 | public DBOperationType OperationType 20 | { 21 | get 22 | { 23 | if (SYS_CHANGE_OPERATION == "I") 24 | { 25 | return DBOperationType.Insert; 26 | } 27 | else if (SYS_CHANGE_OPERATION == "U") 28 | { 29 | return DBOperationType.Update; 30 | } 31 | else if (SYS_CHANGE_OPERATION == "D") 32 | { 33 | return DBOperationType.Delete; 34 | } 35 | else 36 | { 37 | return DBOperationType.None; 38 | } 39 | } 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Entity/EntityHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | 7 | namespace SQLEFTableNotification.Entity 8 | { 9 | public static class EntityHelper 10 | { 11 | 12 | static readonly String _EncryptionKey = "eid729"; 13 | 14 | public static string Encrypt(string clearText) 15 | { 16 | byte[] clearBytes = Encoding.Unicode.GetBytes(clearText); 17 | using (Aes encryptor = Aes.Create()) 18 | { 19 | Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(_EncryptionKey, new byte[] { 0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 }); 20 | encryptor.Key = pdb.GetBytes(32); 21 | encryptor.IV = pdb.GetBytes(16); 22 | using (MemoryStream ms = new MemoryStream()) 23 | { 24 | using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateEncryptor(), CryptoStreamMode.Write)) 25 | { 26 | cs.Write(clearBytes, 0, clearBytes.Length); 27 | cs.Close(); 28 | } 29 | clearText = Convert.ToBase64String(ms.ToArray()); 30 | } 31 | } 32 | return clearText; 33 | } 34 | public static string Decrypt(string cipherText) 35 | { 36 | cipherText = cipherText.Replace(" ", "+"); 37 | byte[] cipherBytes = Convert.FromBase64String(cipherText); 38 | using (Aes encryptor = Aes.Create()) 39 | { 40 | Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(_EncryptionKey, new byte[] { 0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 }); 41 | encryptor.Key = pdb.GetBytes(32); 42 | encryptor.IV = pdb.GetBytes(16); 43 | using (MemoryStream ms = new MemoryStream()) 44 | { 45 | using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateDecryptor(), CryptoStreamMode.Write)) 46 | { 47 | cs.Write(cipherBytes, 0, cipherBytes.Length); 48 | cs.Close(); 49 | } 50 | cipherText = Encoding.Unicode.GetString(ms.ToArray()); 51 | } 52 | } 53 | return cipherText; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Entity/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System.Text; 6 | 7 | namespace SQLEFTableNotification.Entity 8 | { 9 | /// 10 | /// A user attached to an account 11 | /// 12 | public class User : BaseEntity 13 | { 14 | [Required] 15 | [StringLength(20)] 16 | public string FirstName { get; set; } 17 | [Required] 18 | [StringLength(20)] 19 | public string LastName { get; set; } 20 | [StringLength(30)] 21 | public string UserName { get; set; } 22 | [Required] 23 | [StringLength(30)] 24 | public string Email { get; set; } 25 | [StringLength(255)] 26 | public string Description { get; set; } 27 | public bool IsAdminRole { get; set; } 28 | [StringLength(255)] 29 | public string Roles { get; set; } 30 | public bool IsActive { get; set; } 31 | [StringLength(50)] 32 | public string Password { get; set; } //stored encrypted 33 | [NotMapped] 34 | public string DecryptedPassword 35 | { 36 | get { return Decrypt(Password); } 37 | set { Password = Encrypt(value); } 38 | } 39 | public int AccountId { get; set; } 40 | 41 | 42 | public virtual Account Account { get; set; } 43 | 44 | private string Decrypt(string cipherText) 45 | { 46 | return EntityHelper.Decrypt(cipherText); 47 | } 48 | private string Encrypt(string clearText) 49 | { 50 | return EntityHelper.Encrypt(clearText); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Entity/UserChangeTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SQLEFTableNotification.Entity.Entity 8 | { 9 | public class UserChangeTable : ChangeTable, IEntityPk 10 | { 11 | public int Id { get; set; } 12 | } 13 | 14 | public class ChangeTableVersionCount 15 | { 16 | public long VersionCount { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/IEntityPk.cs: -------------------------------------------------------------------------------- 1 | namespace SQLEFTableNotification.Entity 2 | { 3 | public interface IEntityPk 4 | { 5 | public int Id { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Migrations/20180708205028_InitialCreate.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using SQLEFTableNotification.Entity.Context; 8 | using System; 9 | 10 | namespace SQLEFTableNotification.Entity.Migrations 11 | { 12 | [DbContext(typeof(SQLEFTableNotificationContext))] 13 | [Migration("20180708205028_InitialCreate")] 14 | partial class InitialCreate 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("SQLEFTableNotification.Entity.Account", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd() 28 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 29 | 30 | b.Property("Created"); 31 | 32 | b.Property("Description") 33 | .HasMaxLength(255); 34 | 35 | b.Property("Email") 36 | .IsRequired() 37 | .HasMaxLength(30); 38 | 39 | b.Property("IsActive"); 40 | 41 | b.Property("IsTrial"); 42 | 43 | b.Property("Modified"); 44 | 45 | b.Property("Name") 46 | .IsRequired() 47 | .HasMaxLength(30); 48 | 49 | b.Property("SetActive"); 50 | 51 | b.Property("RowVersion") 52 | .IsConcurrencyToken() 53 | .ValueGeneratedOnAddOrUpdate(); 54 | 55 | b.HasKey("Id"); 56 | 57 | b.ToTable("Accounts"); 58 | }); 59 | 60 | modelBuilder.Entity("SQLEFTableNotification.Entity.User", b => 61 | { 62 | b.Property("Id") 63 | .ValueGeneratedOnAdd() 64 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 65 | 66 | b.Property("AccountId"); 67 | 68 | b.Property("Created"); 69 | 70 | b.Property("Description") 71 | .HasMaxLength(255); 72 | 73 | b.Property("Email") 74 | .IsRequired() 75 | .HasMaxLength(30); 76 | 77 | b.Property("FirstName") 78 | .IsRequired() 79 | .HasMaxLength(20); 80 | 81 | b.Property("IsActive"); 82 | 83 | b.Property("IsAdminRole"); 84 | 85 | b.Property("LastName") 86 | .IsRequired() 87 | .HasMaxLength(20); 88 | 89 | b.Property("Modified"); 90 | 91 | b.Property("Password") 92 | .HasMaxLength(50); 93 | 94 | b.Property("Roles") 95 | .HasMaxLength(255); 96 | 97 | b.Property("RowVersion") 98 | .IsConcurrencyToken() 99 | .ValueGeneratedOnAddOrUpdate(); 100 | 101 | b.Property("UserName") 102 | .HasMaxLength(30); 103 | 104 | b.HasKey("Id"); 105 | 106 | b.HasIndex("AccountId"); 107 | 108 | b.ToTable("Users"); 109 | }); 110 | 111 | modelBuilder.Entity("SQLEFTableNotification.Entity.User", b => 112 | { 113 | b.HasOne("SQLEFTableNotification.Entity.Account", "Account") 114 | .WithMany("Users") 115 | .HasForeignKey("AccountId") 116 | .OnDelete(DeleteBehavior.Cascade); 117 | }); 118 | #pragma warning restore 612, 618 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Migrations/20180708205028_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | using System; 4 | 5 | namespace SQLEFTableNotification.Entity.Migrations 6 | { 7 | public partial class InitialCreate : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Accounts", 13 | columns: table => new 14 | { 15 | Id = table.Column(nullable: false) 16 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 17 | Created = table.Column(nullable: false), 18 | Modified = table.Column(nullable: false), 19 | Name = table.Column(maxLength: 30, nullable: false), 20 | Email = table.Column(maxLength: 30, nullable: false), 21 | Description = table.Column(maxLength: 255, nullable: true), 22 | IsTrial = table.Column(nullable: false), 23 | IsActive = table.Column(nullable: false), 24 | SetActive = table.Column(nullable: false), 25 | RowVersion = table.Column(nullable: true, rowVersion: true) 26 | }, 27 | constraints: table => 28 | { 29 | table.PrimaryKey("PK_Accounts", x => x.Id); 30 | }); 31 | 32 | migrationBuilder.CreateTable( 33 | name: "Users", 34 | columns: table => new 35 | { 36 | Id = table.Column(nullable: false) 37 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 38 | Created = table.Column(nullable: false), 39 | Modified = table.Column(nullable: false), 40 | FirstName = table.Column(maxLength: 20, nullable: false), 41 | LastName = table.Column(maxLength: 20, nullable: false), 42 | UserName = table.Column(maxLength: 30, nullable: true), 43 | Email = table.Column(maxLength: 30, nullable: false), 44 | Description = table.Column(maxLength: 255, nullable: true), 45 | IsAdminRole = table.Column(nullable: false), 46 | Roles = table.Column(maxLength: 255, nullable: true), 47 | IsActive = table.Column(nullable: false), 48 | Password = table.Column(maxLength: 255, nullable: true), 49 | AccountId = table.Column(nullable: false), 50 | RowVersion = table.Column(nullable: true, rowVersion: true) 51 | }, 52 | constraints: table => 53 | { 54 | table.PrimaryKey("PK_Users", x => x.Id); 55 | table.ForeignKey( 56 | name: "FK_Users_Accounts_AccountId", 57 | column: x => x.AccountId, 58 | principalTable: "Accounts", 59 | principalColumn: "Id", 60 | onDelete: ReferentialAction.Cascade); 61 | }); 62 | 63 | migrationBuilder.CreateIndex( 64 | name: "IX_Users_AccountId", 65 | table: "Users", 66 | column: "AccountId"); 67 | } 68 | 69 | protected override void Down(MigrationBuilder migrationBuilder) 70 | { 71 | migrationBuilder.DropTable( 72 | name: "Users"); 73 | 74 | migrationBuilder.DropTable( 75 | name: "Accounts"); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Migrations/SQLEFTableNotificationContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using SQLEFTableNotification.Entity.Context; 7 | using System; 8 | 9 | namespace SQLEFTableNotification.Entity.Migrations 10 | { 11 | [DbContext(typeof(SQLEFTableNotificationContext))] 12 | partial class SQLEFTableNotificationContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "3.1.0") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("SQLEFTableNotification.Entity.Account", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("int") 27 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 28 | 29 | b.Property("Created") 30 | .HasColumnType("datetime2"); 31 | 32 | b.Property("Description") 33 | .HasColumnType("nvarchar(255)") 34 | .HasMaxLength(255); 35 | 36 | b.Property("Email") 37 | .IsRequired() 38 | .HasColumnType("nvarchar(30)") 39 | .HasMaxLength(30); 40 | 41 | b.Property("IsActive") 42 | .HasColumnType("bit"); 43 | 44 | b.Property("IsTrial") 45 | .HasColumnType("bit"); 46 | 47 | b.Property("Modified") 48 | .HasColumnType("datetime2"); 49 | 50 | b.Property("Name") 51 | .IsRequired() 52 | .HasColumnType("nvarchar(30)") 53 | .HasMaxLength(30); 54 | 55 | b.Property("RowVersion") 56 | .IsConcurrencyToken() 57 | .ValueGeneratedOnAddOrUpdate() 58 | .HasColumnType("rowversion"); 59 | 60 | b.Property("SetActive") 61 | .HasColumnType("datetime2"); 62 | 63 | b.Property("TestText") 64 | .HasColumnType("nvarchar(50)") 65 | .HasMaxLength(50); 66 | 67 | b.HasKey("Id"); 68 | 69 | b.ToTable("Accounts"); 70 | }); 71 | 72 | modelBuilder.Entity("SQLEFTableNotification.Entity.User", b => 73 | { 74 | b.Property("Id") 75 | .ValueGeneratedOnAdd() 76 | .HasColumnType("int") 77 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 78 | 79 | b.Property("AccountId") 80 | .HasColumnType("int"); 81 | 82 | b.Property("Created") 83 | .HasColumnType("datetime2"); 84 | 85 | b.Property("Description") 86 | .HasColumnType("nvarchar(255)") 87 | .HasMaxLength(255); 88 | 89 | b.Property("Email") 90 | .IsRequired() 91 | .HasColumnType("nvarchar(30)") 92 | .HasMaxLength(30); 93 | 94 | b.Property("FirstName") 95 | .IsRequired() 96 | .HasColumnType("nvarchar(20)") 97 | .HasMaxLength(20); 98 | 99 | b.Property("IsActive") 100 | .HasColumnType("bit"); 101 | 102 | b.Property("IsAdminRole") 103 | .HasColumnType("bit"); 104 | 105 | b.Property("LastName") 106 | .IsRequired() 107 | .HasColumnType("nvarchar(20)") 108 | .HasMaxLength(20); 109 | 110 | b.Property("Modified") 111 | .HasColumnType("datetime2"); 112 | 113 | b.Property("Password") 114 | .HasColumnType("nvarchar(50)") 115 | .HasMaxLength(50); 116 | 117 | b.Property("Roles") 118 | .HasColumnType("nvarchar(255)") 119 | .HasMaxLength(255); 120 | 121 | b.Property("RowVersion") 122 | .IsConcurrencyToken() 123 | .ValueGeneratedOnAddOrUpdate() 124 | .HasColumnType("rowversion"); 125 | 126 | b.Property("TestText") 127 | .HasColumnType("nvarchar(50)") 128 | .HasMaxLength(50); 129 | 130 | b.Property("UserName") 131 | .HasColumnType("nvarchar(30)") 132 | .HasMaxLength(30); 133 | 134 | b.HasKey("Id"); 135 | 136 | b.HasIndex("AccountId"); 137 | 138 | b.ToTable("Users"); 139 | }); 140 | 141 | modelBuilder.Entity("SQLEFTableNotification.Entity.User", b => 142 | { 143 | b.HasOne("SQLEFTableNotification.Entity.Account", "Account") 144 | .WithMany("Users") 145 | .HasForeignKey("AccountId") 146 | .OnDelete(DeleteBehavior.Cascade) 147 | .IsRequired(); 148 | }); 149 | #pragma warning restore 612, 618 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Model/DBOperationType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SQLEFTableNotification.Entity.Model 8 | { 9 | public enum DBOperationType 10 | { 11 | None, 12 | Delete, 13 | Insert, 14 | Update 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Repository/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | 7 | namespace SQLEFTableNotification.Entity.Repository 8 | { 9 | public interface IRepository where T : class 10 | { 11 | IEnumerable GetAll(); 12 | IEnumerable Get(Expression> predicate); 13 | T GetOne(Expression> predicate); 14 | void Insert(T entity); 15 | void Delete(T entity); 16 | void Delete(object id); 17 | void Update(object id, T entity); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Repository/IRepositoryAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | 7 | namespace SQLEFTableNotification.Entity.Repository 8 | { 9 | public interface IRepositoryAsync where T : class 10 | { 11 | Task> GetAll(); 12 | Task> Get(Expression> predicate); 13 | Task GetOne(Expression> predicate); 14 | Task Insert(T entity); 15 | void Delete(T entity); 16 | Task Delete(object id); 17 | Task Update(object id, T entity); 18 | 19 | IQueryable GetEntityWithRawSql(string query, params object[] parameters); 20 | IQueryable GetModelWithRawSql(string query, params object[] parameters) where TViewModel : class; 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Repository/Repository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using SQLEFTableNotification.Entity.Context; 3 | using SQLEFTableNotification.Entity.UnitofWork; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | 10 | namespace SQLEFTableNotification.Entity.Repository 11 | { 12 | /// 13 | /// General repository class 14 | /// 15 | /// 16 | public class Repository : IRepository where T : class 17 | { 18 | private readonly IUnitOfWork _unitOfWork; 19 | public Repository(IUnitOfWork unitOfWork) 20 | { 21 | _unitOfWork = unitOfWork; 22 | } 23 | 24 | 25 | public IEnumerable GetAll() 26 | { 27 | return _unitOfWork.Context.Set(); 28 | } 29 | public IEnumerable Get(System.Linq.Expressions.Expression> predicate) 30 | { 31 | return _unitOfWork.Context.Set().Where(predicate).AsEnumerable(); 32 | } 33 | public T GetOne(System.Linq.Expressions.Expression> predicate) 34 | { 35 | return _unitOfWork.Context.Set().Where(predicate).FirstOrDefault(); 36 | } 37 | public void Insert(T entity) 38 | { 39 | if (entity != null) _unitOfWork.Context.Set().Add(entity); 40 | } 41 | public void Update(object id, T entity) 42 | { 43 | if (entity != null) 44 | { 45 | //T entitytoUpdate = _unitOfWork.Context.Set().Find(id); 46 | //if (entitytoUpdate != null) 47 | // _unitOfWork.Context.Entry(entitytoUpdate).CurrentValues.SetValues(entity); 48 | _unitOfWork.Context.Entry(entity).State = EntityState.Modified; 49 | } 50 | } 51 | public void Delete(object id) 52 | { 53 | T entity = _unitOfWork.Context.Set().Find(id); 54 | Delete(entity); 55 | } 56 | public void Delete(T entity) 57 | { 58 | if (entity != null) _unitOfWork.Context.Set().Remove(entity); 59 | } 60 | 61 | } 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/Repository/RepositoryAsync.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using SQLEFTableNotification.Entity.Context; 3 | using SQLEFTableNotification.Entity.UnitofWork; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using System.Threading.Tasks; 9 | 10 | 11 | namespace SQLEFTableNotification.Entity.Repository 12 | { 13 | /// 14 | /// General repository class async 15 | /// 16 | /// 17 | public class RepositoryAsync : IRepositoryAsync where T : class 18 | { 19 | private readonly IUnitOfWork _unitOfWork; 20 | public RepositoryAsync(IUnitOfWork unitOfWork) 21 | { 22 | _unitOfWork = unitOfWork; 23 | } 24 | 25 | public async Task> GetAll() 26 | { 27 | return await _unitOfWork.Context.Set().ToListAsync(); 28 | } 29 | public async Task> Get(System.Linq.Expressions.Expression> predicate) 30 | { 31 | return await _unitOfWork.Context.Set().Where(predicate).ToListAsync(); 32 | } 33 | 34 | public async Task GetOne(System.Linq.Expressions.Expression> predicate) 35 | { 36 | return await _unitOfWork.Context.Set().Where(predicate).FirstOrDefaultAsync(); 37 | } 38 | public async Task Insert(T entity) 39 | { 40 | if (entity != null) 41 | await _unitOfWork.Context.Set().AddAsync(entity); 42 | } 43 | public async Task Update(object id, T entity) 44 | { 45 | if (entity != null) 46 | { 47 | //T entitytoUpdate = await _unitOfWork.Context.Set().FindAsync(id); 48 | //if (entitytoUpdate != null) 49 | // _unitOfWork.Context.Entry(entitytoUpdate).CurrentValues.SetValues(entity); 50 | _unitOfWork.Context.Entry(entity).State = EntityState.Modified; 51 | } 52 | } 53 | public async Task Delete(object id) 54 | { 55 | T entity = await _unitOfWork.Context.Set().FindAsync(id); 56 | Delete(entity); 57 | } 58 | public void Delete(T entity) 59 | { 60 | if (entity != null) _unitOfWork.Context.Set().Remove(entity); 61 | } 62 | 63 | public IQueryable GetEntityWithRawSql(string query, params object[] parameters) 64 | { 65 | return _unitOfWork.Context.Database.SqlQueryRaw(query, parameters); 66 | } 67 | 68 | public IQueryable GetModelWithRawSql(string query, params object[] parameters) where TViewModel : class 69 | { 70 | return _unitOfWork.Context.Database.SqlQueryRaw(query, parameters); 71 | 72 | } 73 | 74 | 75 | 76 | } 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/SQLEFTableNotification.Entity.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | TextTemplatingFileGenerator 20 | 1_t4EntityHelpersGenerate.cs 21 | 22 | 23 | 24 | 25 | 26 | True 27 | True 28 | 1_t4EntityHelpersGenerate.tt 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/UnitofWork/UnitofWork.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using SQLEFTableNotification.Entity.Context; 3 | using SQLEFTableNotification.Entity.Repository; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace SQLEFTableNotification.Entity.UnitofWork 9 | { 10 | public interface IUnitOfWork : IDisposable 11 | { 12 | 13 | IRepository GetRepository() where TEntity : class; 14 | IRepositoryAsync GetRepositoryAsync() where TEntity : class; 15 | 16 | SQLEFTableNotificationContext Context { get; } 17 | int Save(); 18 | Task SaveAsync(); 19 | } 20 | 21 | public interface IUnitOfWork : IUnitOfWork where TContext : DbContext 22 | { 23 | } 24 | 25 | public class UnitOfWork : IUnitOfWork 26 | { 27 | public SQLEFTableNotificationContext Context { get; } 28 | 29 | private Dictionary _repositoriesAsync; 30 | private Dictionary _repositories; 31 | private bool _disposed; 32 | 33 | public UnitOfWork(SQLEFTableNotificationContext context) 34 | { 35 | Context = context; 36 | _disposed = false; 37 | } 38 | 39 | public IRepository GetRepository() where TEntity : class 40 | { 41 | if (_repositories == null) _repositories = new Dictionary(); 42 | var type = typeof(TEntity); 43 | if (!_repositories.ContainsKey(type)) _repositories[type] = new Repository(this); 44 | return (IRepository)_repositories[type]; 45 | } 46 | 47 | public IRepositoryAsync GetRepositoryAsync() where TEntity : class 48 | { 49 | if (_repositories == null) _repositoriesAsync = new Dictionary(); 50 | var type = typeof(TEntity); 51 | if (!_repositoriesAsync.ContainsKey(type)) _repositoriesAsync[type] = new RepositoryAsync(this); 52 | return (IRepositoryAsync)_repositoriesAsync[type]; 53 | } 54 | 55 | public int Save() 56 | { 57 | try 58 | { 59 | return Context.SaveChanges(); 60 | } 61 | catch (DbUpdateConcurrencyException) 62 | { 63 | return -1; 64 | } 65 | } 66 | public async Task SaveAsync() 67 | { 68 | try 69 | { 70 | return await Context.SaveChangesAsync(); 71 | } 72 | catch (DbUpdateConcurrencyException) 73 | { 74 | return -1; 75 | } 76 | } 77 | 78 | public void Dispose() 79 | { 80 | Dispose(true); 81 | GC.SuppressFinalize(this); 82 | } 83 | public void Dispose(bool isDisposing) 84 | { 85 | if (!_disposed) 86 | { 87 | if (isDisposing) 88 | { 89 | Context.Dispose(); 90 | } 91 | } 92 | _disposed = true; 93 | } 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/_dbfirst.txt: -------------------------------------------------------------------------------- 1 | This extension is optimized for code-first approach. 2 | 3 | This is infromation and script that can be used to load existing database tbales definition to Entity classes. 4 | Loadded db model has to be updated to follow required entity classes structure like: 5 | public class TableName : BaseEntity 6 | 7 | More information: 8 | https://www.entityframeworktutorial.net/efcore/create-model-for-existing-database-in-ef-core.aspx 9 | https://docs.microsoft.com/en-us/ef/core/cli/powershell 10 | 11 | PMC script for db firts scaffolding: 12 | 13 | PM> Scaffold-DbContext "Server=.\SQLExpress;Database=SQLEFTableNotification;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Entity -DataAnnotations -NoPluralize 14 | 15 | PM> Scaffold-DbContext "Server=.\SQLExpress;Database=SQLEFTableNotification;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Entity -DataAnnotations -NoPluralize -Tables "Table1","Table2" 16 | -------------------------------------------------------------------------------- /SQLEFTableNotification/SQLEFTableNotification.Entity/_nugets.txt: -------------------------------------------------------------------------------- 1 | //NOTE: all packages will be installed automatically by VS with proper versions 2 | 3 | https://www.nuget.org/packages/Microsoft.EntityFrameworkCore/ 4 | PM> Install-Package Microsoft.EntityFrameworkCore 5 | 6 | PM> Install-Package Microsoft.EntityFrameworkCore.InMemory 7 | PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer 8 | PM> Install-Package Microsoft.EntityFrameworkCore.Tools 9 | 10 | https://www.nuget.org/packages/Microsoft.AspNet.Identity.EntityFramework/ 11 | //PM>Install-Package Microsoft.AspNet.Identity.EntityFramework 12 | 13 | https://www.newtonsoft.com/json 14 | PM> Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson 15 | 16 | https://docs.microsoft.com/en-us/ef/core/querying/related-data 17 | https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Proxies 18 | PM> Install-Package Microsoft.EntityFrameworkCore.Proxies 19 | 20 | https://www.learnentityframeworkcore.com/migrations 21 | 22 | --add new migration 23 | PM>add-migration AddNewMigration 24 | --remove last one 25 | PM>remove-migration 26 | --update db 27 | PM>update-database 28 | --update database to specific migration 29 | PM>update-database AddNewMigration 30 | 31 | STEPS: 32 | PM> update-database 20180708205028_InitialCreate 33 | PM> add-migration AddNewMigration 34 | PM> update-database 35 | 36 | 37 | /// Designed by AnaSoft Inc. 2019-2020 38 | /// http://www.anasoft.net/apincore 39 | /// 40 | /// Download full version from http://www.anasoft.net/apincore with these added features: 41 | /// -XUnit integration tests project (update the connection string and run tests) 42 | /// -API Postman tests as json file 43 | /// -JWT and IS4 authentication tests 44 | /// -T4 for smart code generation for new entities views, services, controllers and tests 45 | 46 | -------------------------------------------------------------------------------- /SQLEFTableNotificationLib/Delegates/ServiceDelegates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using SQLEFTableNotification.Models; 7 | 8 | namespace SQLEFTableNotification.Delegates 9 | { 10 | 11 | public delegate void ErrorEventHandler(object sender, Models.ErrorEventArgs e); 12 | public delegate void ChangedEventHandler(object sender, RecordChangedEventArgs e) where T : class, new(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /SQLEFTableNotificationLib/Interfaces/IChangeTableService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SQLEFTableNotification.Interfaces 8 | { 9 | public interface IChangeTableService 10 | { 11 | Task> GetRecords(string CommandText); 12 | List GetRecordsSync(string CommandText); 13 | 14 | long GetRecordCountSync(string CommandText); 15 | 16 | Task GetRecordCount(string CommandText); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SQLEFTableNotificationLib/Interfaces/IDBNotificationService.cs: -------------------------------------------------------------------------------- 1 | using SQLEFTableNotification.Delegates; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SQLEFTableNotification.Interfaces 9 | { 10 | public interface IDBNotificationService where T : class, new() 11 | { 12 | event Delegates.ErrorEventHandler OnError; 13 | 14 | event ChangedEventHandler OnChanged; 15 | 16 | //event StatusEventHandler OnStatusChanged; 17 | 18 | // SetFilterExpression(Expression> expression); 19 | Task StartNotify(); 20 | Task StopNotify(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SQLEFTableNotificationLib/Models/DBOperationType.cs: -------------------------------------------------------------------------------- 1 | namespace SQLEFTableNotification.Models 2 | { 3 | public enum TableOperationStatus 4 | { 5 | None, 6 | Starting, 7 | Started, 8 | WaitingForNotification, 9 | StopDueToCancellation, 10 | StopDueToError 11 | } 12 | 13 | public enum DBOperationType 14 | { 15 | None, 16 | Delete, 17 | Insert, 18 | Update 19 | } 20 | } -------------------------------------------------------------------------------- /SQLEFTableNotificationLib/Models/ErrorEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SQLEFTableNotification.Models 8 | { 9 | public class ErrorEventArgs //: DBBaseEventArgs 10 | { 11 | #region Properties 12 | 13 | public string Message { get; } 14 | 15 | public Exception Error { get; protected set; } 16 | 17 | #endregion 18 | 19 | #region Constructors 20 | 21 | public ErrorEventArgs( 22 | Exception e) 23 | //string server, 24 | //string database, 25 | //string sender) : this("SQL monitoring stopped working", e, server, database, sender) 26 | { 27 | Error = e; 28 | } 29 | 30 | public ErrorEventArgs( 31 | string message, 32 | Exception e) 33 | //string server, 34 | //string database, 35 | //string sender) : base(server, database, sender) 36 | { 37 | Message = message; 38 | Error = e; 39 | } 40 | 41 | #endregion 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SQLEFTableNotificationLib/Models/RecordChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace SQLEFTableNotification.Models 2 | { 3 | public class RecordChangedEventArgs where T : class, new() 4 | { 5 | 6 | public List Entities { get; protected set; } 7 | 8 | public DBOperationType ChangeType { get; protected set; } 9 | 10 | 11 | public RecordChangedEventArgs(DBOperationType changeType, List entities) 12 | { 13 | ChangeType = changeType; 14 | Entities = entities; 15 | } 16 | 17 | 18 | public RecordChangedEventArgs(List entities) 19 | { 20 | Entities = entities; 21 | } 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /SQLEFTableNotificationLib/SQLEFTableNotification.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | True 8 | Monitor and receive notifications on record table change 9 | $(AssemblyName) 10 | $([System.DateTime]::Now.Year) Jatin Dave 11 | 12 | 1.1.0.0 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /SQLEFTableNotificationLib/Services/ScheduledJobTimer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SQLEFTableNotification.Services 9 | { 10 | public sealed class ScheduledJobTimer : IDisposable 11 | { 12 | #region Fields 13 | private Timer _timer; 14 | private int _isRunning; 15 | private Action _job; 16 | #endregion 17 | 18 | #region Properties 19 | public bool AutoDispose { get; set; } 20 | public TimeSpan DueTime { get; private set; } 21 | public TimeSpan Period { get; private set; } 22 | public DateTime PreviousExecuteTime { get; private set; } 23 | public DateTime NextExecuteTime { get; private set; } 24 | #endregion 25 | 26 | #region Methods 27 | private ScheduledJobTimer(Action job, DateTime dueTime, TimeSpan period) 28 | { 29 | Contract.Requires(job != null); 30 | 31 | _job = job; 32 | if (dueTime != DateTime.MinValue) 33 | { 34 | DueTime = dueTime - DateTime.Now; 35 | } 36 | if (DueTime < TimeSpan.Zero) 37 | { 38 | DueTime = TimeSpan.Zero; 39 | } 40 | Period = period; 41 | 42 | AutoDispose = true; 43 | //App.DisposeService.Register(this.GetType(), this); 44 | } 45 | public ScheduledJobTimer(Action job, DateTime dueTime) 46 | : this(job, dueTime, Timeout.InfiniteTimeSpan) 47 | { 48 | 49 | } 50 | public ScheduledJobTimer(Action job, TimeSpan period) 51 | : this(job, DateTime.Now.AddSeconds(2d), period) 52 | { 53 | 54 | } 55 | 56 | public void Execute(object state) 57 | { 58 | if (Interlocked.Exchange(ref _isRunning, 1) == 0) 59 | { 60 | try 61 | { 62 | _job(state); 63 | } 64 | catch (Exception ex) 65 | { 66 | //App.LogError(ex, "JobTimer"); 67 | } 68 | finally 69 | { 70 | PreviousExecuteTime = DateTime.Now; 71 | if (Period == Timeout.InfiniteTimeSpan) 72 | { 73 | NextExecuteTime = DateTime.MinValue; 74 | if (AutoDispose) 75 | { 76 | Dispose(); 77 | } 78 | } 79 | else 80 | { 81 | NextExecuteTime = PreviousExecuteTime + Period; 82 | } 83 | 84 | Interlocked.Exchange(ref _isRunning, 0); 85 | } 86 | } 87 | } 88 | 89 | public void Start(object state = null) 90 | { 91 | if (_timer == null) 92 | { 93 | _timer = new Timer(callback: Execute, state, DueTime, Period); 94 | } 95 | else 96 | { 97 | _timer.Change(DueTime, Period); 98 | } 99 | } 100 | 101 | public void Stop() 102 | { 103 | if (_timer == null) 104 | { 105 | return; 106 | } 107 | _timer.Change(Timeout.Infinite, Timeout.Infinite); 108 | } 109 | 110 | public void Dispose() 111 | { 112 | if (_job != null) 113 | { 114 | _job = null; 115 | if (_timer != null) 116 | { 117 | _timer.Dispose(); 118 | _timer = null; 119 | } 120 | 121 | //App.DisposeService.Release(this.GetType(), this); 122 | } 123 | } 124 | #endregion 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /SQLEFTableNotificationLib/Services/SqlDBNotificationService.cs: -------------------------------------------------------------------------------- 1 | using SQLEFTableNotification.Delegates; 2 | using SQLEFTableNotification.Interfaces; 3 | using SQLEFTableNotification.Models; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace SQLEFTableNotification.Services 10 | { 11 | /// 12 | /// Monitor table record changes at specified interval and notify changes to subscriber. 13 | /// 14 | /// 15 | public class SqlDBNotificationService : IDBNotificationService, IDisposable where TChangeTableEntity : class, new() 16 | { 17 | /// 18 | /// Refer articles https://www.mssqltips.com/sqlservertip/6762/sql-server-change-tracking-track-columns-updated/ 19 | /// 20 | 21 | #region members 22 | private const string Sql_CurrentVersion = @"SELECT ISNULL(CHANGE_TRACKING_CURRENT_VERSION(), 0) as VersionCount"; 23 | private const string Sql_TrackingOnChangeTable = @"SELECT ct.* FROM CHANGETABLE(CHANGES {0},{1}) ct WHERE ct.SYS_CHANGE_VERSION <= {2}"; 24 | private readonly string _changeContextName; 25 | private long _currentVersion; 26 | private ScheduledJobTimer _timer; 27 | private readonly TimeSpan _period; 28 | private readonly IChangeTableService _changeTableService; 29 | private readonly string _tableName; 30 | private int _errorCount = 0; 31 | private readonly string? _connectionString = null; 32 | 33 | //private List _model; 34 | #endregion 35 | 36 | #region Events 37 | public event Delegates.ErrorEventHandler OnError; 38 | 39 | public event ChangedEventHandler OnChanged; 40 | 41 | //public event StatusEventHandler OnStatusChanged; 42 | #endregion 43 | 44 | #region Constructor 45 | /// 46 | /// Constructor 47 | /// 48 | /// database table name to monitor 49 | /// database connection string with credentials 50 | /// ChangeTableService which can read data from database. 51 | /// Specify initial SYS_CHANGE_VERSION 52 | /// Specify interval 53 | /// record identifier to avoid database changes for own(not implemented). 54 | public SqlDBNotificationService(string tableName, string connectionString, IChangeTableService changeTableService, long version = -1L, TimeSpan? period = null, string recordIdentifier = "WMWebAPI") 55 | { 56 | _changeContextName = recordIdentifier; 57 | _currentVersion = version; 58 | _period = period.GetValueOrDefault(TimeSpan.FromSeconds(60D)); 59 | _changeTableService = changeTableService; 60 | _tableName = tableName; 61 | _errorCount = 0; 62 | _connectionString = connectionString; 63 | } 64 | 65 | private async Task QueryCurrentVersion() 66 | { 67 | return await _changeTableService.GetRecordCount(Sql_CurrentVersion); 68 | } 69 | #endregion 70 | private async Task SqlDBNotificationService_OnError(object sender, Models.ErrorEventArgs e) 71 | { 72 | if (OnError != null) 73 | OnError(sender, e); 74 | } 75 | 76 | private async Task SqlDBNotificationService_OnChanged(object sender, RecordChangedEventArgs e) 77 | { 78 | if (OnChanged != null) 79 | OnChanged(sender, e); 80 | } 81 | 82 | public void Dispose() 83 | { 84 | 85 | } 86 | 87 | public async Task StartNotify() 88 | { 89 | _errorCount = 0; 90 | _currentVersion = _currentVersion == -1L ? await QueryCurrentVersion() : _currentVersion; 91 | _timer = new ScheduledJobTimer(JobContent, _period); 92 | _timer.Start(); 93 | 94 | } 95 | 96 | private void EnableChangeTracking() 97 | { 98 | //Execute Query or stored procedure to enable change tracking on database and table level. 99 | } 100 | 101 | public async Task StopNotify() 102 | { 103 | await Task.Run(() => _timer.Stop()); 104 | } 105 | 106 | private async void JobContent(object state) 107 | { 108 | try 109 | { 110 | long lastVersion = await QueryCurrentVersion(); 111 | if (_currentVersion == lastVersion) 112 | { 113 | return; 114 | } 115 | 116 | var buffer = new StringBuilder(); 117 | 118 | var commandText = string.Format(Sql_TrackingOnChangeTable, _tableName, _currentVersion, lastVersion, _changeContextName); 119 | List records = await _changeTableService.GetRecords(commandText); 120 | if (records != null && records.Count > 0) 121 | { 122 | RecordChangedEventArgs recordChangedEventArgs = new RecordChangedEventArgs(records);// obj); 123 | await SqlDBNotificationService_OnChanged(this, recordChangedEventArgs); 124 | } 125 | _currentVersion = lastVersion; 126 | } 127 | catch (Exception ex) 128 | { 129 | await SqlDBNotificationService_OnError(this, new Models.ErrorEventArgs(ex)); 130 | _errorCount++; 131 | } 132 | finally 133 | { 134 | if (_errorCount > 20) 135 | { 136 | _timer.Stop(); 137 | await SqlDBNotificationService_OnError(this, new Models.ErrorEventArgs(new Exception($"Stopped monitoring for {_connectionString}:{_tableName}"))); 138 | } 139 | //do nothing. 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /SQLEFTableNotificationLib/SqlEFTableDependency.cd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | QAAAAAABASGgAAAAAIAAAAAAgACAAABYAAAAAAAaAIA= 10 | Services\SqlDBNotificationService.cs 11 | 12 | 13 | 14 | 15 | 16 | 17 | AEAAAAAAAIAgAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAA= 18 | Interfaces\IChangeTableService.cs 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /SQLEFTableNotificationTests/SQLEFTableNotification.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /SQLEFTableNotificationTests/Services/SqlDBNotificationServiceTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SQLEFTableNotification.Tests.Services 9 | { 10 | [TestClass()] 11 | public class SqlDBNotificationServiceTests 12 | { 13 | [TestMethod()] 14 | public void SqlDBNotificationServiceTest() 15 | { 16 | Assert.Fail(); 17 | } 18 | 19 | [TestMethod()] 20 | public void DisposeTest() 21 | { 22 | Assert.Fail(); 23 | } 24 | 25 | [TestMethod()] 26 | public void StartNotifyTest() 27 | { 28 | Assert.Fail(); 29 | } 30 | 31 | [TestMethod()] 32 | public void StopNotifyTest() 33 | { 34 | Assert.Fail(); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /SQLTableNotification.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.6.33829.357 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLEFTableNotification.Api", "SQLEFTableNotification\SQLEFTableNotification.Api\SQLEFTableNotification.Api.csproj", "{5FF78FB5-5EC2-4072-9DC7-27214BB4CD14}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLEFTableNotification.Domain", "SQLEFTableNotification\SQLEFTableNotification.Domain\SQLEFTableNotification.Domain.csproj", "{F5026CAF-BAD8-49D3-A71C-9E0EF2E4CBAE}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLEFTableNotification.Entity", "SQLEFTableNotification\SQLEFTableNotification.Entity\SQLEFTableNotification.Entity.csproj", "{5B5EAC8B-6280-4A6C-B7A9-55F1AA1B69A7}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLEFTableNotification.Console", "SQLEFTableNotification.Console\SQLEFTableNotification.Console.csproj", "{16C1CEE8-C8FB-4FEB-8E83-8A69837B3AF4}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLEFTableNotification", "SQLEFTableNotificationLib\SQLEFTableNotification.csproj", "{FF0EADA9-50F9-4DBF-A701-2BC48D6B1E70}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLEFTableNotification.Tests", "SQLEFTableNotificationTests\SQLEFTableNotification.Tests.csproj", "{A09658A9-297E-4918-9241-0CB14C13F778}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {5FF78FB5-5EC2-4072-9DC7-27214BB4CD14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {5FF78FB5-5EC2-4072-9DC7-27214BB4CD14}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {5FF78FB5-5EC2-4072-9DC7-27214BB4CD14}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {5FF78FB5-5EC2-4072-9DC7-27214BB4CD14}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {F5026CAF-BAD8-49D3-A71C-9E0EF2E4CBAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {F5026CAF-BAD8-49D3-A71C-9E0EF2E4CBAE}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {F5026CAF-BAD8-49D3-A71C-9E0EF2E4CBAE}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {F5026CAF-BAD8-49D3-A71C-9E0EF2E4CBAE}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {5B5EAC8B-6280-4A6C-B7A9-55F1AA1B69A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {5B5EAC8B-6280-4A6C-B7A9-55F1AA1B69A7}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {5B5EAC8B-6280-4A6C-B7A9-55F1AA1B69A7}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {5B5EAC8B-6280-4A6C-B7A9-55F1AA1B69A7}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {16C1CEE8-C8FB-4FEB-8E83-8A69837B3AF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {16C1CEE8-C8FB-4FEB-8E83-8A69837B3AF4}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {16C1CEE8-C8FB-4FEB-8E83-8A69837B3AF4}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {16C1CEE8-C8FB-4FEB-8E83-8A69837B3AF4}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {FF0EADA9-50F9-4DBF-A701-2BC48D6B1E70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {FF0EADA9-50F9-4DBF-A701-2BC48D6B1E70}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {FF0EADA9-50F9-4DBF-A701-2BC48D6B1E70}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {FF0EADA9-50F9-4DBF-A701-2BC48D6B1E70}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {A09658A9-297E-4918-9241-0CB14C13F778}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {A09658A9-297E-4918-9241-0CB14C13F778}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {A09658A9-297E-4918-9241-0CB14C13F778}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {A09658A9-297E-4918-9241-0CB14C13F778}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {29B04D5E-04DB-4BF1-8DE3-49F6445BD722} 54 | EndGlobalSection 55 | EndGlobal 56 | --------------------------------------------------------------------------------