Happy new year! Just had to document this quickly. Accessing internal members is necessary for testing, but also sometimes for production code. I found this to be the case optimising a day-end process which read some database records copied them elsewhere and needed to save. Unfortunately by doing so resets the connection object inside the persistence layer (an old proprietary ORM unchangeable right now). This meant that the records then needed to be re-read, to prevent access to stale objects that would not have future changes updated into the database.
This was fixed by calling an internal method on the connection object that registers database entities into an internal collection. A bit of a hack, but fixes the performance issue without getting caught up in fixing a persistence layer that will be replaced soon.
But there were two possible approaches.
1) You can grant another library permission to access its internal (or friend in vb) members;
2) Or you can allow access to a internal member with an extension method.
Grant access to all internals
Lets start with granting access to another library to access the internal members. This will give the other library full access to all internal members in the source library. This will only work if both libraries are signed with a strong name. To find out how to do that go here.
Open the library (DLL) that contains the internal members you would like to access externally. Find the assemblyinfo.vb (or .cs in c#) and add the following line at the bottom:
[assembly: InternalsVisibleTo("MyUnitTestLibrary, PublicKey=002400000480000094000000060200000024000052534131000400000100010015b2942f80eea6e7c6ee614974fefc2820236c27c4c0dd221c6469178e7e34884466e5dfb8f1958f0feff6dc71f00176927ce3a3d360b50c08880efa9de0a8cb33a80d4cf3ed6849a5f1d0699b64346f94b505c2b6c585b1e0dd3929640ad3f23ce35a0f79e6539dd74f1ab9f25c9a366124f1d84117126b7c5d83e37fbc6ddb")]
You will need to import "Imports System.Runtime.CompilerServices".
Replace the "MyUnitTestLibrary" with the name of the other library where you need to call the internal methods of this library.
The next thing to do, is to obtain the public key signature data. This is done by using the Sn.exe command line utility.
Use the following command line (in which the parameters are case sensitive):
sn -Tp YourDllNameHere.dll
Output should look something like this:
Microsoft (R) .NET Framework Strong Name Utility Version 3.5.30729.1
Copyright (c) Microsoft Corporation. All rights reserved.
Public key is
002400000480000094000000060200000024000052534131000400000100010015b2942f80eea6
e7c6ee614974fefc2820236c27c4c0dd221c6469178e7e34884466e5dfb8f1958f0feff6dc71f0
0176927ce3a3d360b50c08880efa9de0a8cb33a80d4cf3ed6849a5f1d0699b64346f94b505c2b6
c585b1e0dd3929640ad3f23ce35a0f79e6539dd74f1ab9f25c9a366124f1d84117126b7c5d83e3
7fbc6ddb
Public key token is ee02eaa036161368
|
Copy and past the public key data into the assemblyinfo file, obviously removing the carriage returns.
Build both libraries and you can now access the internals. This is normally fine for testing but to expose all internals for only one use case feels dirty for production code.
Grant access to one method using an extension
Heres the class that owns the internal member.
Public Class Connection
Friend Sub RegisterObject(newObject As BaseEntity)
' Inner workings not important
End Sub
End Class
And the use case from the other library...
Dim objects As IEnumerable(Of BaseEntity) = connection.GetObjects(...) 'How to get objects is unimportant...
Me.PerformSomeOperation(objects)
connection.Commit() ' This resets the internal collection and the above array of objects is now stale and unusable, all changes to them will not be saved.
For Each(Dim x As BaseEntity In objects)
connection.RegisterObject(x) ' Need to be able to do something like this, but can't right now.
Next
The plan is to add an extension to the library that contains the extension.
I
mports System.Runtime.CompilerServices
Namespace Extensions
Public Module ContainerJobExtensions
<Extension()> _
Public Sub RegisterObjects(ByVal instance As Connection, ByVal objects As BaseEntity())
Array.ForEach(Of BaseEntity)(objects, AddressOf instance.RegisterObject)
End Sub
End Module
End Namespace
Now with that I can re-register the objects with the connection like so...
Dim objects As IEnumerable(Of BaseEntity) = connection.GetObjects(...) 'How to get objects is unimportant...
Me.PerformSomeOperation(objects)
connection.Commit() ' This resets the internal collection and the above array of objects is now stale and unusable, all changes to them will not be saved.
connection.RegisterObjects(objects)
By putting the extension in another namespace you can selectively import this only into the use case where you need to use it.
Done!
Incidentally this attribute works when an assembly is not signed also. Just omit the public key parameter.
ReplyDeleteIn addition to this, your mocking library will also need access to the internal types in your assembly. If it does not it will not be able to mock other internal dependencies of the subject under test. I generally use RhinoMocks, so here is the syntax to give RhinoMocks access to the internals:
ReplyDelete[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Rhino.Mocks dynamically generated mocksare created in this assembly