├── .github
└── FUNDING.yml
├── .gitignore
├── DapperSample
├── DapperSample.csproj
├── IStudentRepository.cs
├── Student.cs
└── StudentRepository.cs
├── DapperSampleTest
├── DapperSampleTest.csproj
└── StudentRepositoryTest.cs
├── LICENSE
├── MockableStaticGenerator.sln
├── MockableStaticGenerator
├── Constants.cs
├── MockableGenerator.cs
├── MockableStaticGenerator.csproj
├── SourceGeneratorExtensions.cs
└── SyntaxReceiver.cs
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: HamedFathi
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,intellij+all
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio,visualstudiocode,intellij+all
4 |
5 | ### Intellij+all ###
6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
8 |
9 | # User-specific stuff
10 | .idea/**/workspace.xml
11 | .idea/**/tasks.xml
12 | .idea/**/usage.statistics.xml
13 | .idea/**/dictionaries
14 | .idea/**/shelf
15 |
16 | # Generated files
17 | .idea/**/contentModel.xml
18 |
19 | # Sensitive or high-churn files
20 | .idea/**/dataSources/
21 | .idea/**/dataSources.ids
22 | .idea/**/dataSources.local.xml
23 | .idea/**/sqlDataSources.xml
24 | .idea/**/dynamic.xml
25 | .idea/**/uiDesigner.xml
26 | .idea/**/dbnavigator.xml
27 |
28 | # Gradle
29 | .idea/**/gradle.xml
30 | .idea/**/libraries
31 |
32 | # Gradle and Maven with auto-import
33 | # When using Gradle or Maven with auto-import, you should exclude module files,
34 | # since they will be recreated, and may cause churn. Uncomment if using
35 | # auto-import.
36 | # .idea/artifacts
37 | # .idea/compiler.xml
38 | # .idea/jarRepositories.xml
39 | # .idea/modules.xml
40 | # .idea/*.iml
41 | # .idea/modules
42 | # *.iml
43 | # *.ipr
44 |
45 | # CMake
46 | cmake-build-*/
47 |
48 | # Mongo Explorer plugin
49 | .idea/**/mongoSettings.xml
50 |
51 | # File-based project format
52 | *.iws
53 |
54 | # IntelliJ
55 | out/
56 |
57 | # mpeltonen/sbt-idea plugin
58 | .idea_modules/
59 |
60 | # JIRA plugin
61 | atlassian-ide-plugin.xml
62 |
63 | # Cursive Clojure plugin
64 | .idea/replstate.xml
65 |
66 | # Crashlytics plugin (for Android Studio and IntelliJ)
67 | com_crashlytics_export_strings.xml
68 | crashlytics.properties
69 | crashlytics-build.properties
70 | fabric.properties
71 |
72 | # Editor-based Rest Client
73 | .idea/httpRequests
74 |
75 | # Android studio 3.1+ serialized cache file
76 | .idea/caches/build_file_checksums.ser
77 |
78 | ### Intellij+all Patch ###
79 | # Ignores the whole .idea folder and all .iml files
80 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
81 |
82 | .idea/
83 |
84 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
85 |
86 | *.iml
87 | modules.xml
88 | .idea/misc.xml
89 | *.ipr
90 |
91 | # Sonarlint plugin
92 | .idea/sonarlint
93 |
94 | ### VisualStudioCode ###
95 | .vscode/*
96 | !.vscode/tasks.json
97 | !.vscode/launch.json
98 | *.code-workspace
99 |
100 | ### VisualStudioCode Patch ###
101 | # Ignore all local history of files
102 | .history
103 | .ionide
104 |
105 | ### VisualStudio ###
106 | ## Ignore Visual Studio temporary files, build results, and
107 | ## files generated by popular Visual Studio add-ons.
108 | ##
109 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
110 |
111 | # User-specific files
112 | *.rsuser
113 | *.suo
114 | *.user
115 | *.userosscache
116 | *.sln.docstates
117 |
118 | # User-specific files (MonoDevelop/Xamarin Studio)
119 | *.userprefs
120 |
121 | # Mono auto generated files
122 | mono_crash.*
123 |
124 | # Build results
125 | [Dd]ebug/
126 | [Dd]ebugPublic/
127 | [Rr]elease/
128 | [Rr]eleases/
129 | x64/
130 | x86/
131 | [Aa][Rr][Mm]/
132 | [Aa][Rr][Mm]64/
133 | bld/
134 | [Bb]in/
135 | [Oo]bj/
136 | [Ll]og/
137 | [Ll]ogs/
138 |
139 | # Visual Studio 2015/2017 cache/options directory
140 | .vs/
141 | # Uncomment if you have tasks that create the project's static files in wwwroot
142 | #wwwroot/
143 |
144 | # Visual Studio 2017 auto generated files
145 | Generated\ Files/
146 |
147 | # MSTest test Results
148 | [Tt]est[Rr]esult*/
149 | [Bb]uild[Ll]og.*
150 |
151 | # NUnit
152 | *.VisualState.xml
153 | TestResult.xml
154 | nunit-*.xml
155 |
156 | # Build Results of an ATL Project
157 | [Dd]ebugPS/
158 | [Rr]eleasePS/
159 | dlldata.c
160 |
161 | # Benchmark Results
162 | BenchmarkDotNet.Artifacts/
163 |
164 | # .NET Core
165 | project.lock.json
166 | project.fragment.lock.json
167 | artifacts/
168 |
169 | # StyleCop
170 | StyleCopReport.xml
171 |
172 | # Files built by Visual Studio
173 | *_i.c
174 | *_p.c
175 | *_h.h
176 | *.ilk
177 | *.meta
178 | *.obj
179 | *.iobj
180 | *.pch
181 | *.pdb
182 | *.ipdb
183 | *.pgc
184 | *.pgd
185 | *.rsp
186 | *.sbr
187 | *.tlb
188 | *.tli
189 | *.tlh
190 | *.tmp
191 | *.tmp_proj
192 | *_wpftmp.csproj
193 | *.log
194 | *.vspscc
195 | *.vssscc
196 | .builds
197 | *.pidb
198 | *.svclog
199 | *.scc
200 |
201 | # Chutzpah Test files
202 | _Chutzpah*
203 |
204 | # Visual C++ cache files
205 | ipch/
206 | *.aps
207 | *.ncb
208 | *.opendb
209 | *.opensdf
210 | *.sdf
211 | *.cachefile
212 | *.VC.db
213 | *.VC.VC.opendb
214 |
215 | # Visual Studio profiler
216 | *.psess
217 | *.vsp
218 | *.vspx
219 | *.sap
220 |
221 | # Visual Studio Trace Files
222 | *.e2e
223 |
224 | # TFS 2012 Local Workspace
225 | $tf/
226 |
227 | # Guidance Automation Toolkit
228 | *.gpState
229 |
230 | # ReSharper is a .NET coding add-in
231 | _ReSharper*/
232 | *.[Rr]e[Ss]harper
233 | *.DotSettings.user
234 |
235 | # TeamCity is a build add-in
236 | _TeamCity*
237 |
238 | # DotCover is a Code Coverage Tool
239 | *.dotCover
240 |
241 | # AxoCover is a Code Coverage Tool
242 | .axoCover/*
243 | !.axoCover/settings.json
244 |
245 | # Coverlet is a free, cross platform Code Coverage Tool
246 | coverage*[.json, .xml, .info]
247 |
248 | # Visual Studio code coverage results
249 | *.coverage
250 | *.coveragexml
251 |
252 | # NCrunch
253 | _NCrunch_*
254 | .*crunch*.local.xml
255 | nCrunchTemp_*
256 |
257 | # MightyMoose
258 | *.mm.*
259 | AutoTest.Net/
260 |
261 | # Web workbench (sass)
262 | .sass-cache/
263 |
264 | # Installshield output folder
265 | [Ee]xpress/
266 |
267 | # DocProject is a documentation generator add-in
268 | DocProject/buildhelp/
269 | DocProject/Help/*.HxT
270 | DocProject/Help/*.HxC
271 | DocProject/Help/*.hhc
272 | DocProject/Help/*.hhk
273 | DocProject/Help/*.hhp
274 | DocProject/Help/Html2
275 | DocProject/Help/html
276 |
277 | # Click-Once directory
278 | publish/
279 |
280 | # Publish Web Output
281 | *.[Pp]ublish.xml
282 | *.azurePubxml
283 | # Note: Comment the next line if you want to checkin your web deploy settings,
284 | # but database connection strings (with potential passwords) will be unencrypted
285 | *.pubxml
286 | *.publishproj
287 |
288 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
289 | # checkin your Azure Web App publish settings, but sensitive information contained
290 | # in these scripts will be unencrypted
291 | PublishScripts/
292 |
293 | # NuGet Packages
294 | *.nupkg
295 | # NuGet Symbol Packages
296 | *.snupkg
297 | # The packages folder can be ignored because of Package Restore
298 | **/[Pp]ackages/*
299 | # except build/, which is used as an MSBuild target.
300 | !**/[Pp]ackages/build/
301 | # Uncomment if necessary however generally it will be regenerated when needed
302 | #!**/[Pp]ackages/repositories.config
303 | # NuGet v3's project.json files produces more ignorable files
304 | *.nuget.props
305 | *.nuget.targets
306 |
307 | # Microsoft Azure Build Output
308 | csx/
309 | *.build.csdef
310 |
311 | # Microsoft Azure Emulator
312 | ecf/
313 | rcf/
314 |
315 | # Windows Store app package directories and files
316 | AppPackages/
317 | BundleArtifacts/
318 | Package.StoreAssociation.xml
319 | _pkginfo.txt
320 | *.appx
321 | *.appxbundle
322 | *.appxupload
323 |
324 | # Visual Studio cache files
325 | # files ending in .cache can be ignored
326 | *.[Cc]ache
327 | # but keep track of directories ending in .cache
328 | !?*.[Cc]ache/
329 |
330 | # Others
331 | ClientBin/
332 | ~$*
333 | *~
334 | *.dbmdl
335 | *.dbproj.schemaview
336 | *.jfm
337 | *.pfx
338 | *.publishsettings
339 | orleans.codegen.cs
340 |
341 | # Including strong name files can present a security risk
342 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
343 | #*.snk
344 |
345 | # Since there are multiple workflows, uncomment next line to ignore bower_components
346 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
347 | #bower_components/
348 |
349 | # RIA/Silverlight projects
350 | Generated_Code/
351 |
352 | # Backup & report files from converting an old project file
353 | # to a newer Visual Studio version. Backup files are not needed,
354 | # because we have git ;-)
355 | _UpgradeReport_Files/
356 | Backup*/
357 | UpgradeLog*.XML
358 | UpgradeLog*.htm
359 | ServiceFabricBackup/
360 | *.rptproj.bak
361 |
362 | # SQL Server files
363 | *.mdf
364 | *.ldf
365 | *.ndf
366 |
367 | # Business Intelligence projects
368 | *.rdl.data
369 | *.bim.layout
370 | *.bim_*.settings
371 | *.rptproj.rsuser
372 | *- [Bb]ackup.rdl
373 | *- [Bb]ackup ([0-9]).rdl
374 | *- [Bb]ackup ([0-9][0-9]).rdl
375 |
376 | # Microsoft Fakes
377 | FakesAssemblies/
378 |
379 | # GhostDoc plugin setting file
380 | *.GhostDoc.xml
381 |
382 | # Node.js Tools for Visual Studio
383 | .ntvs_analysis.dat
384 | node_modules/
385 |
386 | # Visual Studio 6 build log
387 | *.plg
388 |
389 | # Visual Studio 6 workspace options file
390 | *.opt
391 |
392 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
393 | *.vbw
394 |
395 | # Visual Studio LightSwitch build output
396 | **/*.HTMLClient/GeneratedArtifacts
397 | **/*.DesktopClient/GeneratedArtifacts
398 | **/*.DesktopClient/ModelManifest.xml
399 | **/*.Server/GeneratedArtifacts
400 | **/*.Server/ModelManifest.xml
401 | _Pvt_Extensions
402 |
403 | # Paket dependency manager
404 | .paket/paket.exe
405 | paket-files/
406 |
407 | # FAKE - F# Make
408 | .fake/
409 |
410 | # CodeRush personal settings
411 | .cr/personal
412 |
413 | # Python Tools for Visual Studio (PTVS)
414 | __pycache__/
415 | *.pyc
416 |
417 | # Cake - Uncomment if you are using it
418 | # tools/**
419 | # !tools/packages.config
420 |
421 | # Tabs Studio
422 | *.tss
423 |
424 | # Telerik's JustMock configuration file
425 | *.jmconfig
426 |
427 | # BizTalk build output
428 | *.btp.cs
429 | *.btm.cs
430 | *.odx.cs
431 | *.xsd.cs
432 |
433 | # OpenCover UI analysis results
434 | OpenCover/
435 |
436 | # Azure Stream Analytics local run output
437 | ASALocalRun/
438 |
439 | # MSBuild Binary and Structured Log
440 | *.binlog
441 |
442 | # NVidia Nsight GPU debugger configuration file
443 | *.nvuser
444 |
445 | # MFractors (Xamarin productivity tool) working folder
446 | .mfractor/
447 |
448 | # Local History for Visual Studio
449 | .localhistory/
450 |
451 | # BeatPulse healthcheck temp database
452 | healthchecksdb
453 |
454 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
455 | MigrationBackup/
456 |
457 | # Ionide (cross platform F# VS Code tools) working folder
458 | .ionide/
459 |
460 | # End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,intellij+all
--------------------------------------------------------------------------------
/DapperSample/DapperSample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/DapperSample/IStudentRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace DapperSample
4 | {
5 | public interface IStudentRepository
6 | {
7 | IEnumerable GetStudents();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/DapperSample/Student.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace DapperSample
4 | {
5 | public class Student
6 | {
7 | public int Id { get; set; }
8 | public string Name { get; set; }
9 | public string Family { get; set; }
10 | public DateTime BirthDate { get; set; }
11 | }
12 | }
--------------------------------------------------------------------------------
/DapperSample/StudentRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Data;
3 | using System;
4 | using MockableGenerated;
5 |
6 | namespace DapperSample
7 | {
8 | [MockableStatic(typeof(Dapper.SqlMapper))]
9 | public class StudentRepository : IStudentRepository
10 | {
11 | private readonly IDbConnection _dbConnection;
12 | private readonly ISqlMapperWrapper _dapperSqlMapper;
13 |
14 | public StudentRepository(IDbConnection dbConnection, ISqlMapperWrapper dapperSqlMapper)
15 | {
16 | _dbConnection = dbConnection;
17 | _dapperSqlMapper = dapperSqlMapper;
18 | }
19 |
20 | public IEnumerable GetStudents()
21 | {
22 | return _dapperSqlMapper.Query(_dbConnection, "SELECT * FROM STUDENT");
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/DapperSampleTest/DapperSampleTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | runtime; build; native; contentfiles; analyzers; buildtransitive
15 | all
16 |
17 |
18 | runtime; build; native; contentfiles; analyzers; buildtransitive
19 | all
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/DapperSampleTest/StudentRepositoryTest.cs:
--------------------------------------------------------------------------------
1 | using DapperSample;
2 | using Moq;
3 | using System.Data;
4 | using Xunit;
5 | using MockableGenerated;
6 |
7 | namespace DapperSampleTest
8 | {
9 | public class StudentRepositoryTest
10 | {
11 | [Fact]
12 | public void STUDENT_REPOSITORY_TEST()
13 | {
14 | var mockConn = new Mock();
15 | var mockDapper = new Mock();
16 | var sut = new StudentRepository(mockConn.Object, mockDapper.Object);
17 | var stu = sut.GetStudents();
18 | Assert.NotNull(stu);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Hamed Fathi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MockableStaticGenerator.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30711.63
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MockableStaticGenerator", "MockableStaticGenerator\MockableStaticGenerator.csproj", "{B4FC6ABC-FD08-4668-9311-5BB9835BF4FE}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DapperSample", "DapperSample\DapperSample.csproj", "{6C5C27DF-3C3F-47F8-A7DC-9085F6C7F571}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DapperSampleTest", "DapperSampleTest\DapperSampleTest.csproj", "{7848AA88-8F75-4751-8F6A-4310A06330B3}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {B4FC6ABC-FD08-4668-9311-5BB9835BF4FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {B4FC6ABC-FD08-4668-9311-5BB9835BF4FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {B4FC6ABC-FD08-4668-9311-5BB9835BF4FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {B4FC6ABC-FD08-4668-9311-5BB9835BF4FE}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {6C5C27DF-3C3F-47F8-A7DC-9085F6C7F571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {6C5C27DF-3C3F-47F8-A7DC-9085F6C7F571}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {6C5C27DF-3C3F-47F8-A7DC-9085F6C7F571}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {6C5C27DF-3C3F-47F8-A7DC-9085F6C7F571}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {7848AA88-8F75-4751-8F6A-4310A06330B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {7848AA88-8F75-4751-8F6A-4310A06330B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {7848AA88-8F75-4751-8F6A-4310A06330B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {7848AA88-8F75-4751-8F6A-4310A06330B3}.Release|Any CPU.Build.0 = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {03A53FA6-D0DE-400C-92EA-3138522A51A6}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/MockableStaticGenerator/Constants.cs:
--------------------------------------------------------------------------------
1 | namespace MockableStaticGenerator
2 | {
3 | internal static class Constants
4 | {
5 | internal const string MockableStaticAttribute = @"
6 | namespace System
7 | {
8 | [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
9 | public sealed class MockableStaticAttribute : Attribute
10 | {
11 | public MockableStaticAttribute(Type type)
12 | {
13 | }
14 | public MockableStaticAttribute()
15 | {
16 | }
17 | }
18 | }";
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/MockableStaticGenerator/MockableGenerator.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using Microsoft.CodeAnalysis.Text;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Text;
9 |
10 | namespace MockableStaticGenerator
11 | {
12 | [Generator]
13 | public class MockableGenerator : ISourceGenerator
14 | {
15 | private static readonly List _interfaces = new List();
16 | private static readonly List _classes = new List();
17 | public class MethodSymbolVisitor : SymbolVisitor
18 | {
19 | private readonly string _typeName;
20 |
21 | public MethodSymbolVisitor(string typeName)
22 | {
23 | _typeName = typeName;
24 | }
25 | public override void VisitNamespace(INamespaceSymbol symbol)
26 | {
27 | foreach (var child in symbol.GetMembers())
28 | {
29 | child.Accept(this);
30 | }
31 | }
32 |
33 | public override void VisitNamedType(INamedTypeSymbol symbol)
34 | {
35 | foreach (var child in symbol.GetMembers())
36 | {
37 | child.Accept(this);
38 | }
39 | }
40 |
41 | public override void VisitMethod(IMethodSymbol symbol)
42 | {
43 | var cls = symbol.ReceiverType;
44 | var isClass = symbol.ReceiverType != null && symbol.ReceiverType.TypeKind == TypeKind.Class;
45 | var isPublic = symbol.ReceiverType != null && string.Equals(symbol.ReceiverType.DeclaredAccessibility.ToString().ToLowerInvariant(), "public", StringComparison.InvariantCultureIgnoreCase);
46 | if (isClass && isPublic && symbol.IsStatic && symbol.MethodKind == MethodKind.Ordinary)
47 | {
48 | var className = cls.Name;
49 | var classNameWithNs = cls.ToDisplayString();
50 | if (classNameWithNs != _typeName) return;
51 |
52 | var wrapperClassName = !className.Contains('<') ? className + "Wrapper" : className.Replace("<", "Wrapper<");
53 | var classTypeParameters = ((INamedTypeSymbol)cls).GetTypeParameters();
54 | var wrapperInterfaceName = $"I{wrapperClassName}{classTypeParameters}";
55 | var constraintClauses = ((INamedTypeSymbol)cls).GetConstraintClauses();
56 | var baseList = ((INamedTypeSymbol)cls).GetBaseList(wrapperInterfaceName);
57 | var returnKeyword = symbol.ReturnsVoid ? "" : "return ";
58 | var methodSignature = symbol.GetSignatureText();
59 | var callableMethodSignature = symbol.GetCallableSignatureText();
60 | var obsoleteAttribute = symbol.GetAttributes().FirstOrDefault(x => x.ToString().StartsWith("System.ObsoleteAttribute("))?.ToString();
61 |
62 | var interfaceSource = $"\tpublic interface I{wrapperClassName}{classTypeParameters} {constraintClauses} {{";
63 | var classSource = $"\tpublic class {wrapperClassName}{classTypeParameters} {baseList} {constraintClauses} {{";
64 |
65 |
66 | if (!_interfaces.Contains(interfaceSource))
67 | _interfaces.Add(interfaceSource);
68 |
69 | if (!_classes.Contains(classSource))
70 | _classes.Add(classSource);
71 |
72 | if (!_interfaces.Contains(methodSignature))
73 | {
74 | _interfaces.Add($"\t\t{methodSignature};");
75 | }
76 |
77 | if (!_classes.Contains(methodSignature))
78 | {
79 | var obs = !string.IsNullOrEmpty(obsoleteAttribute) ? $"\t\t[{obsoleteAttribute}]" : "\t\t";
80 | _classes.Add($"{obs} public {methodSignature} {{ {returnKeyword}{classNameWithNs}.{callableMethodSignature}; }}");
81 | }
82 | }
83 | }
84 | }
85 |
86 | public void Execute(GeneratorExecutionContext context)
87 | {
88 | context.AddSource(nameof(Constants.MockableStaticAttribute), SourceText.From(Constants.MockableStaticAttribute, Encoding.UTF8));
89 |
90 | if (context.SyntaxReceiver is not SyntaxReceiver receiver)
91 | return;
92 |
93 | CSharpParseOptions options = (context.Compilation as CSharpCompilation)?.SyntaxTrees[0].Options as CSharpParseOptions;
94 | Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(Constants.MockableStaticAttribute, Encoding.UTF8), options));
95 | INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName($"System.{nameof(Constants.MockableStaticAttribute)}");
96 |
97 | var sources = new HashSet();
98 | sources.Clear();
99 | var assemblyName = "";
100 | foreach (var cls in receiver.Classes)
101 | {
102 | if (_interfaces.Count > 0)
103 | {
104 | _interfaces.Add($"\t}}");
105 | }
106 | if (_classes.Count > 0)
107 | {
108 | _classes.Add($"\t}}");
109 | }
110 |
111 |
112 | SemanticModel model = compilation.GetSemanticModel(cls.SyntaxTree);
113 | var clsSymbol = model.GetDeclaredSymbol(cls);
114 |
115 | var attr = clsSymbol.GetAttributes().FirstOrDefault(ad => ad.AttributeClass != null && ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
116 |
117 | if (attr == null) continue;
118 | var isParameterlessCtor = attr?.ConstructorArguments.Length == 0;
119 |
120 | var sbInterface = new StringBuilder();
121 | var sbClass = new StringBuilder();
122 |
123 | if (isParameterlessCtor)
124 | {
125 | var methods = cls.DescendantNodes().OfType().Where(x => x.IsPublic() && x.IsStatic()).ToList();
126 | if (methods.Count == 0) continue;
127 |
128 | var className = clsSymbol.Name;
129 | var ns = string.IsNullOrEmpty(cls.GetNamespace()) ? "" : cls.GetNamespace() + ".";
130 | var baseList = string.IsNullOrEmpty(cls.BaseList?.ToFullString()) ? ":" : cls.BaseList?.ToFullString().Trim() + ",";
131 | assemblyName = clsSymbol.ContainingAssembly.Identity.Name;
132 | var wrapperClassName = !className.Contains('<') ? className + "Wrapper" : className.Replace("<", "Wrapper<");
133 | var classTypeParameters = cls.GetTypeParameters() ?? "";
134 | var constraintClauses = cls.GetConstraintClauses() ?? "";
135 | sbInterface.AppendLine($"\tpublic interface I{wrapperClassName}{classTypeParameters} {constraintClauses} {{");
136 | sbClass.AppendLine($"\tpublic class {wrapperClassName}{classTypeParameters} {baseList} I{wrapperClassName}{classTypeParameters} {constraintClauses} {{");
137 |
138 | foreach (MethodDeclarationSyntax method in methods)
139 | {
140 | var text = method.GetSignatureText();
141 |
142 | if (!sbInterface.ToString().Contains(text))
143 | sbInterface.AppendLine($"\t\t{text};");
144 |
145 | if (!sbClass.ToString().Contains(text))
146 | {
147 | var returnKeyword = method.ReturnsVoid() ? "" : "return ";
148 | var isObsolete = method.TryGetObsoleteAttribute(out var obsoleteAttrText);
149 | if (isObsolete)
150 | sbClass.AppendLine($"\t\t{obsoleteAttrText}");
151 |
152 | sbClass.AppendLine($"\t\tpublic {method.GetSignatureText()} {{");
153 | sbClass.AppendLine($"\t\t\t{returnKeyword}{ns}{className}{classTypeParameters}.{method.GetCallableSignatureText()};");
154 | sbClass.AppendLine($"\t\t}}");
155 | }
156 | }
157 |
158 | sbInterface.AppendLine($"\t}}");
159 | sbClass.AppendLine($"\t}}");
160 | }
161 | else
162 | {
163 | var ctor = (INamedTypeSymbol)attr?.ConstructorArguments[0].Value;
164 | if (ctor != null)
165 | {
166 | var assemblySymbol = ctor.ContainingAssembly.GlobalNamespace;
167 | assemblyName = ctor.ContainingAssembly.Identity.Name;
168 | var visitor = new MethodSymbolVisitor(ctor.ToDisplayString());
169 | visitor.Visit(assemblySymbol);
170 | }
171 |
172 | sbInterface.AppendLine(_interfaces.Distinct().Aggregate((a, b) => a + Environment.NewLine + b) + Environment.NewLine + "\t}");
173 | sbClass.AppendLine(_classes.Distinct().Aggregate((a, b) => a + Environment.NewLine + b) + Environment.NewLine + "\t}");
174 | }
175 |
176 | var interfaceWrapper = sbInterface.ToString();
177 | var classWrapper = sbClass.ToString();
178 |
179 | sources.Add(interfaceWrapper);
180 | sources.Add(classWrapper);
181 | }
182 |
183 | if (_interfaces[_interfaces.Count - 1].Trim() != "}")
184 | {
185 | _interfaces.Add($"\t}}");
186 | }
187 | if (_classes[_classes.Count - 1].Trim() != "}")
188 | {
189 | _classes.Add($"\t}}");
190 | }
191 |
192 | var defaultUsings = new StringBuilder();
193 | defaultUsings.AppendLine("using System;");
194 | defaultUsings.AppendLine("using System.Collections.Generic;");
195 | defaultUsings.AppendLine("using System.Linq;");
196 | defaultUsings.AppendLine("using System.Text;");
197 | defaultUsings.AppendLine("using System.Threading.Tasks;");
198 | var usings = defaultUsings.ToString();
199 |
200 | var src = sources.TakeLast(2).Aggregate((x, y) => x + Environment.NewLine + y).ToString();
201 | var @namespace = new StringBuilder();
202 | @namespace.AppendLine(usings);
203 | @namespace.AppendLine($"namespace MockableGenerated {{");
204 | @namespace.AppendLine(src);
205 | @namespace.Append("}");
206 | var result = @namespace.ToString();
207 | context.AddSource($"MockableGenerated", SourceText.From(result, Encoding.UTF8));
208 | }
209 |
210 | public void Initialize(GeneratorInitializationContext context)
211 | {
212 | //System.Diagnostics.Debugger.Launch();
213 | context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
214 | }
215 |
216 |
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/MockableStaticGenerator/MockableStaticGenerator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0
4 | 0.0.5
5 | MockableStaticGenerator
6 | latest
7 | true
8 | false
9 | Hamed Fathi
10 | A C# source generator to help developers to write interface and class wrappers for whole static methods of an assembly for mocking easier.
11 | N/A
12 | https://github.com/HamedFathi/MockableStaticGenerator
13 | Copyright 2020 - Hamed Fathi
14 | false
15 | MIT
16 | dotnet
17 | true
18 | true
19 | true
20 | $(BaseIntermediateOutputPath)Generated
21 |
22 |
23 | https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json ;$(RestoreAdditionalProjectSources)
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | all
40 | runtime; build; native; contentfiles; analyzers; buildtransitive
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/MockableStaticGenerator/SourceGeneratorExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp.Syntax;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Collections.Immutable;
5 | using System.Linq;
6 | using System.Text.RegularExpressions;
7 |
8 | namespace Microsoft.CodeAnalysis
9 | {
10 | internal static class SourceGeneratorExtensions
11 | {
12 | internal static string ToStringValue(this RefKind refKind)
13 | {
14 | if (refKind == RefKind.RefReadOnly) return "ref readonly";
15 | switch (refKind)
16 | {
17 | case RefKind.Ref:
18 | return "ref";
19 | case RefKind.Out:
20 | return "out";
21 | case RefKind.In:
22 | return "in";
23 | default:
24 | return "";
25 | }
26 | }
27 |
28 | internal static bool IsPublic(this ISymbol symbol)
29 | {
30 | return string.Equals(symbol.DeclaredAccessibility.ToString(), "public", StringComparison.InvariantCultureIgnoreCase);
31 | }
32 |
33 | internal static string GetTypeParameters(this ClassDeclarationSyntax classDeclarationSyntax)
34 | {
35 | var result = classDeclarationSyntax.TypeParameterList?.ToFullString().Trim();
36 | return string.IsNullOrEmpty(result) ? null : result;
37 | }
38 |
39 | internal static string GetConstraintClauses(this ClassDeclarationSyntax classDeclarationSyntax)
40 | {
41 | var result = classDeclarationSyntax.ConstraintClauses.ToFullString().Trim();
42 | return string.IsNullOrEmpty(result) ? null : result;
43 | }
44 |
45 | internal static bool IsPublic(this MethodDeclarationSyntax methodDeclarationSyntax)
46 | {
47 | return methodDeclarationSyntax.Modifiers.Select(x => x.ValueText).Contains("public");
48 | }
49 | internal static bool IsStatic(this MethodDeclarationSyntax methodDeclarationSyntax)
50 | {
51 | return methodDeclarationSyntax.Modifiers.Select(x => x.ValueText).Contains("static");
52 | }
53 |
54 | internal static bool ReturnsVoid(this MethodDeclarationSyntax methodDeclarationSyntax)
55 | {
56 | return methodDeclarationSyntax.ReturnType.ToFullString().Trim() == "void";
57 | }
58 | internal static IEnumerable TakeLast(this IEnumerable source, int n)
59 | {
60 | return source.Skip(Math.Max(0, source.Count() - n));
61 | }
62 | internal static string GetSignatureText(this MethodDeclarationSyntax methodDeclarationSyntax)
63 | {
64 | var name = methodDeclarationSyntax.Identifier.ValueText;
65 | var parameters = methodDeclarationSyntax.ParameterList?.ToFullString().Trim();
66 | var typeParameters = methodDeclarationSyntax.TypeParameterList?.ToFullString().Trim();
67 | var constraintClauses = methodDeclarationSyntax.ConstraintClauses.ToFullString().Replace(System.Environment.NewLine, "").Trim();
68 | var returnType = methodDeclarationSyntax.ReturnType.ToFullString().Trim();
69 |
70 | return $"{returnType} {name}{typeParameters}{parameters} {constraintClauses}".Trim();
71 | }
72 |
73 |
74 | internal static string GetParametersText(this ParameterListSyntax parameterListSyntax)
75 | {
76 | if (parameterListSyntax == null || parameterListSyntax.Parameters.Count == 0) return "()";
77 | var result = new List();
78 | foreach (var item in parameterListSyntax.Parameters)
79 | {
80 | var variableName = item.Identifier;
81 | var modifiers = item.Modifiers.Select(x => x.ValueText).ToList();
82 | var modifiersText = modifiers.Count == 0 ? "" : modifiers.Aggregate((a, b) => a + " " + b);
83 | result.Add($"{modifiersText} {variableName}");
84 | }
85 | return result.Count == 0 ? "()" : $"({result.Aggregate((a, b) => a + ", " + b).Trim()})";
86 | }
87 |
88 | internal static string GetCallableSignatureText(this MethodDeclarationSyntax methodDeclarationSyntax)
89 | {
90 | var name = methodDeclarationSyntax.Identifier.ValueText;
91 | var parameters = methodDeclarationSyntax.ParameterList.GetParametersText();
92 | var typeParameters = methodDeclarationSyntax.TypeParameterList?.ToFullString().Trim();
93 |
94 | return $"{name}{typeParameters}{parameters}".Trim();
95 | }
96 |
97 | internal static bool TryGetObsoleteAttribute(this MethodDeclarationSyntax methodDeclarationSyntax, out string text)
98 | {
99 | var attr = methodDeclarationSyntax.AttributeLists.Where(x => x is not null && IsObsolete(x.GetText().ToString())).Select(x => x.GetText().ToString()).ToList();
100 |
101 | text = attr.Count != 0 ? ReplaceFirst(attr[0].Trim(), "Obsolete", "System.Obsolete") : "";
102 | return attr.Count != 0;
103 |
104 | bool IsObsolete(string text)
105 | {
106 | Match match = Regex.Match(text, @"\[\s*Obsolete[Attribute]*\s*\(");
107 | return match.Success;
108 | }
109 | string ReplaceFirst(string text, string search, string replace)
110 | {
111 | int pos = text.IndexOf(search);
112 | if (pos < 0)
113 | {
114 | return text;
115 | }
116 | return text.Substring(0, pos) + replace + text.Substring(pos + search.Length);
117 | }
118 | }
119 |
120 | internal static string GetNamespace(this SyntaxNode syntaxNode)
121 | {
122 | return syntaxNode.Parent switch
123 | {
124 | NamespaceDeclarationSyntax namespaceDeclarationSyntax => namespaceDeclarationSyntax.Name.ToString(),
125 | null => string.Empty,
126 | _ => GetNamespace(syntaxNode.Parent)
127 | };
128 | }
129 |
130 | internal static string GetTypeParameters(this INamedTypeSymbol namedTypeSymbol)
131 | {
132 | return namedTypeSymbol.TypeParameters.Length == 0 ? ""
133 | : $"<{namedTypeSymbol.TypeParameters.Select(x => x.Name).Aggregate((a, b) => $"{a}, {b}")}>";
134 | }
135 |
136 | internal static string GetConstraintClauses(this INamedTypeSymbol namedTypeSymbol)
137 | {
138 | if (namedTypeSymbol.TypeParameters.Length == 0) return "";
139 | var result = new List();
140 | foreach (var item in namedTypeSymbol.TypeParameters)
141 | {
142 | var constraintType = item.ToDisplayString();
143 | var constraintItems = item.ConstraintTypes.Select(x => x.ToDisplayString()).Aggregate((a, b) => $"{a}, {b}").Trim();
144 | result.Add($"where {constraintType} : {constraintItems}".Trim());
145 | }
146 |
147 | return result.Aggregate((a, b) => $"{a} {b}").Trim();
148 | }
149 |
150 | internal static string GetBaseList(this INamedTypeSymbol namedTypeSymbol, params string[] others)
151 | {
152 | var result = new List();
153 | if (namedTypeSymbol.BaseType != null && !string.Equals(namedTypeSymbol.BaseType.Name, "object", StringComparison.InvariantCultureIgnoreCase))
154 | result.Add(namedTypeSymbol.BaseType.Name);
155 | if (namedTypeSymbol.AllInterfaces.Length != 0)
156 | {
157 | foreach (var item in namedTypeSymbol.AllInterfaces)
158 | {
159 | result.Add(item.Name);
160 | }
161 | }
162 | if (others != null && others.Length != 0)
163 | {
164 | foreach (var item in others)
165 | {
166 | if (!string.IsNullOrEmpty(item))
167 | result.Add(item);
168 | }
169 | }
170 | return result.Count == 0 ? "" : $": {result.Aggregate((a, b) => $"{a}, {b}")}".Trim();
171 | }
172 |
173 | private static string getKind(IParameterSymbol parameterSymbol)
174 | {
175 | return parameterSymbol.IsParams ? "params" : parameterSymbol.RefKind.ToStringValue();
176 | }
177 |
178 | private static string getDefaultValue(IParameterSymbol parameterSymbol)
179 | {
180 | if (parameterSymbol.HasExplicitDefaultValue)
181 | {
182 | if (parameterSymbol.ExplicitDefaultValue == null)
183 | return $" = null";
184 | if (parameterSymbol.ExplicitDefaultValue is bool)
185 | return $" = {parameterSymbol.ExplicitDefaultValue.ToString().ToLowerInvariant()}";
186 | if (parameterSymbol.ExplicitDefaultValue is string)
187 | return $" = \"{parameterSymbol.ExplicitDefaultValue}\"";
188 | else
189 | return $" = {parameterSymbol.ExplicitDefaultValue}";
190 | }
191 | return "";
192 | }
193 |
194 | private static string getConstraintClauses(ITypeParameterSymbol typeParameterSymbol)
195 | {
196 | if (typeParameterSymbol.ConstraintTypes.Length > 0)
197 | {
198 | var constraintType = typeParameterSymbol.ToDisplayString();
199 | var constraintItems = typeParameterSymbol.ConstraintTypes.Select(x => x.ToDisplayString()).Aggregate((a, b) => $"{a}, {b}").Trim();
200 | return $"where {constraintType} : {constraintItems}".Trim();
201 | }
202 | return "";
203 | }
204 | internal static string GetSignatureText(this IMethodSymbol methodSymbol)
205 | {
206 |
207 | var name = methodSymbol.Name;
208 |
209 | var parametersText = methodSymbol.Parameters.Length == 0 ? "()"
210 | : "(" + methodSymbol.Parameters.Select(x => getKind(x) + $" {x.Type} " + x.Name + getDefaultValue(x))
211 | .Aggregate((a, b) => a + ", " + b).Trim() + ")";
212 |
213 | var returnType = methodSymbol.ReturnsVoid ? "void" : methodSymbol.ReturnType.ToDisplayString();
214 | var typeParameters = methodSymbol.TypeParameters.Length == 0
215 | ? ""
216 | : "<" + methodSymbol.TypeParameters.Select(x => x.Name).Aggregate((a, b) => $"{a}, {b}").Trim() + ">";
217 | var constraintClauses = methodSymbol.TypeParameters.Length == 0
218 | ? ""
219 | : methodSymbol.TypeParameters.Select(x => getConstraintClauses(x)).Aggregate((a, b) => $"{a} {b}")
220 | ;
221 |
222 | return $"{returnType} {name}{typeParameters}{parametersText} {constraintClauses}".Trim();
223 | }
224 |
225 | internal static string GetCallableSignatureText(this IMethodSymbol methodSymbol)
226 | {
227 | var name = methodSymbol.Name;
228 |
229 | var parametersText = methodSymbol.Parameters.Length == 0 ? "()"
230 | : "(" + methodSymbol.Parameters.Select(x => $"{getKind(x)} {x.Name}")
231 | .Aggregate((a, b) => $"{a}, {b}").Trim() + ")";
232 | var typeParameters = methodSymbol.TypeParameters.Length == 0
233 | ? ""
234 | : "<" + methodSymbol.TypeParameters.Select(x => x.Name).Aggregate((a, b) => $"{a}, {b}").Trim() + ">";
235 |
236 | return $"{name}{typeParameters}{parametersText}".Trim();
237 | }
238 | }
239 | }
--------------------------------------------------------------------------------
/MockableStaticGenerator/SyntaxReceiver.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp.Syntax;
3 | using System.Collections.Generic;
4 |
5 | namespace MockableStaticGenerator
6 | {
7 | internal class SyntaxReceiver : ISyntaxReceiver
8 | {
9 | internal HashSet Classes { get; } = new();
10 |
11 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
12 | {
13 | if (syntaxNode is ClassDeclarationSyntax { AttributeLists.Count: > 0 } classDeclarationSyntax)
14 | {
15 |
16 | Classes.Add(classDeclarationSyntax);
17 | }
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ## Problem
4 |
5 | I want to mock static or extensions methods. What should I do?
6 |
7 | Sometimes I have those static methods inside my library and sometime they are in an external library like [Dapper](https://github.com/DapperLib/Dapper)
8 |
9 | Do you have any solution?
10 |
11 | ## Solution
12 |
13 | A good, well-known solution is writing a wrapper.
14 |
15 | Consider the following example:
16 |
17 | ```cs
18 | public class MyClass
19 | {
20 | public void LogMessage(string message)
21 | {
22 | Logger.Write(message);
23 | }
24 | }
25 |
26 | public class Logger
27 | {
28 | public static void Write(string message)
29 | {
30 | //Write your code here to log data
31 | }
32 | }
33 | ```
34 |
35 | You can use [Moq](https://github.com/moq/moq4) to mock non-static methods but it cannot be used to mock static methods, so what should I do if I want to mock `Logger.Write`?
36 |
37 | #### Create a wrapper class and interface
38 |
39 | Make an interface just like the signature of the static method
40 |
41 | ```cs
42 | public interface IWrapper
43 | {
44 | void LogData(string message);
45 | }
46 | ```
47 |
48 | Implement that interface and call the static method (`Logger.Write`) into the same non-static method.
49 |
50 | ```cs
51 | public class LogWrapper : IWrapper
52 | {
53 | string _message = null;
54 | public LogWrapper(string message)
55 | {
56 | _message = message;
57 | }
58 | public void LogData(string message)
59 | {
60 | _message = message;
61 | Logger.Write(_message);
62 | }
63 | }
64 | ```
65 |
66 | Now you can use `IWrapper` and `LogWrapper` everywhere in your code and also make things mockable.
67 |
68 | ```cs
69 | var mock = new Mock();
70 | mock.Setup(x => x.LogData(It.IsAny()));
71 | new ProductBL(mock.Object).LogMessage("Hello World!");
72 | mock.VerifyAll();
73 | ```
74 |
75 | #### What is the wrong part with this solution?
76 |
77 | Of course, you can not do this for all static methods or for all external libraries. It is hard and tedious work. Maybe sometimes impossible!
78 |
79 | ## MockableStaticGenerator
80 |
81 | `MockableStaticGenerator` is a C# code generator for making your static/extension methods even from an external library mockable just like above approach but automatically!
82 |
83 | #### How it works?
84 |
85 | * For internal usage
86 |
87 | If you have a class with static methods inside it, put `[MockableStatic]` on top of the class
88 |
89 | ```cs
90 | // For your class with some static methods.
91 | [MockableStatic]
92 | public class Math
93 | {
94 | public static int Add(int a, int b) { return a+b; }
95 | public static int Sub(int a, int b) { return a+b; }
96 | }
97 | ```
98 |
99 | * For external usage
100 |
101 | If you have a class with static methods inside it but from somewhere else (an external library for example), you should introduce **type of** that class to the `[MockableStatic]`
102 |
103 | ```cs
104 | // For type of an external assembly with a lot of static methods
105 | // like 'Dapper.SqlMapper' class in Dapper external library.
106 | // It does not matter on top of what class it is, but Program class can be appropriate.
107 | [MockableStatic(typeof(Dapper.SqlMapper))]
108 | class Program
109 | {
110 | static void Main(string[] args)
111 | {
112 | // ...
113 | }
114 | }
115 | ```
116 |
117 | #### How to call the generated interfaces and classes?
118 |
119 | There are two naming conventions you should follow:
120 |
121 | * `I{CLASS_NAME}Wrapper` for the interface
122 | * `{CLASS_NAME}Wrapper` for the class
123 |
124 | and you can find them under this namespace.
125 |
126 | * namespace `MockableGenerated`
127 | * `MockableGenerated.I{CLASS_NAME}Wrapper`
128 | * `MockableGenerated.{CLASS_NAME}Wrapper`
129 |
130 |
131 | 
132 | 
133 |
134 | #### Attention
135 |
136 | You **must** use the generated interfaces and classes **instead of** the originals to make your library/application testable and mockable.
137 |
138 |
139 |
140 | #### [Nuget](https://www.nuget.org/packages/MockableStaticGenerator)
141 |
142 | [](https://opensource.org/licenses/MIT)
143 | 
144 | 
145 |
146 | ```
147 | Install-Package MockableStaticGenerator
148 | dotnet add package MockableStaticGenerator
149 | ```
150 |
151 | For more details read [this blog post](https://hamedfathi.me/the-dotnet-world-csharp-source-generator/).
152 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------