It is not always possible to rewrite your C or C++ legacy applications in .NET languages in one shot. Sometimes you have to do it module by module, step by step. In other words, you may have to use unmanaged DLLs in your .NET applications.Suppose you want to an unmanaged DLL, name Foo.dll, in your C# applications. What approach would your choose?
If you only want to use a small number of C functions expose by the DLL and the parameters can be marshalled across the managed and unmanaged boundary, you could use P/Invoke.
If you want to use a large number of the functions exposed by the DLL, and/or the some function parameters are hard to map to C# structs, or if the DLL exposes C++ interfaces or C++ classes, you will have to create another layer using C++/CLI as the bridge between the managed application and the unmanaged DLL. The bridge layer would be a DLL that expose .NET classes that you can use in the .NET application. The .NET classes normally delegate the real jobs to the unmanaged DLL.
The approach works great until you want to replace the Foo.dll with a purely managed DLL. The new managed DLL must provide exactly the same classes of the C++/CLI bridge, otherwise, you will have to modify the managed application. Even if you provide the same classes in the managed DLL, you still have to recompile the .NET application. This is all because the .NET application and the bridge are tightly coupled.
I would propose another approach, illustrated in the following component diagram. Foo.Contract.dll defines the interface that the bridge should implement. Foo.Bridge.dll implements the interface by delegating the real jobs to Foo.dll. There is no link-time dependency between the .NET application and Foo.Bridge.dll. The .NET application will load Foo.Bridge.dll at runtime.
Foo.Contract.dll should only defines interfaces, enums, structs, exception classes, and sealed classes if necessary. We should define the interfaces as if we are designing a reusable API, focusing on how to make it easier for the clients to use, rather than how to make it easy to be implemented. Ideally, the contract should define a rich domain model to make it easier to understand and evolve. And there should be a façade interface (or a factory interface), acting as the entry point into the domain model.
Foo.Bridge.dll implements the interfaced defined in Foo.Contract.dll by delegating to Foo.dll.
As mentioned earlier, the .NET application doesn’t link to Foo.Bridge.dll directly, instead it loads the DLL at runtime. All it needs to know are the DLL name and the name of the class that implements the façade interface. We could add an entry in the appSettings section of the configuration file. For example:
<configuration> <appSettings> <add key="Foo.IFacade" value="Foo.Bridge.dll, FacadeImpl" /> </appSettings> </configuration>
The key is the façade interface name. The value contains the DLL name and the class name. At runtime, the .NET application loads the DLL, finds the Type object representing the Façade class, and calls the Activator.CreateInstance() method to create an instance of the class and casts to Foo.IFacade. From then on, you can use all functionality defined in Foo.Contract.dll.
If, in future, we want to replace Foo.dll with a pure .NET DLL, say, Foo.Managed.dll (that implements the interfaces defined in Foo.Contract.dll), we can simply modify the setting to
<add key="Foo.IFacade" value="Foo.Managed.dll, FooFacade" />
And the application should just work without re-complication.
This approach allows us to replace unmanaged DLLs with managed DLLs without having to modify or even recompile the application, thus achieving “close to modification and open to extension” (the open-closed principle). And it forces us to design a sound API, which also makes the bridge unit-testable.