Introduction
Name Explanation
The name of the package "Roxy
" is a mix of two words: "Roslyn
" and "Proxy
" (even though I intend to make this package much more than a just a proxy generator).
Why and What is Roxy
The main purpose of Roxy is to introduce a better separation of concerns and correspondingly simplify the code.
Here are the tasks that the Roxy package addresses now and in the future.
Now:
- Converting an interface into a class using built-in conversion or easily created custom conversion mechanisms.
- Creating adaptors that adapt multiple classes to an interface or an abstract class or both.
- Achieving a greater separation of concerns by mixing behaviors with the objects that they modify.
- Easily accessing non-public properties and methods of 3rd party components.
- Creating smart mixins and allowing to easily swap implementations of the wrapped parts. This greatly enhances testing - e.g., you will be able to easily swap the real backend connection for a mock one.
In the future, I plan to make Roxy a full blown IoC container. In particular, it will allow:
- Resolving interfaces and abstract classes to concrete pre-specified (or generated) types
- Producing singleton objects
- Easily replacing interface or abstract class implementation with a different one
Also, in the future, I plan to remove some of the current Roxy limitations:
- Allowing to generate generic (not fully resolved) classes for generic interfaces
- Allowing to deal with classes and interface that use method overloading
Background
Over many years in software development, I came up with many ideas related to basic programming concepts like interface and implementation inheritance, mixins, adaptors, and relationships between the whole and its parts (e.g. between a class and parts that it contains).
For some time, I thought that compiler change is necessary in order to implement these ideas. I still think that many of these ideas would be better and more efficiently implemented within the compiler by introducing new compiler capabilities and making changes to the language (whether it is C# or Java or C++). Nevertheless, the advent of Roslyn in C# created a great potential for generating and compiling code within the program itself or by Visual Studio plugins. Correspondingly, there are two approaches to code generation: creating an IoC container that will also take care of generating the code based on parameters passed to it, or creating a single file generator tool VS plug in that will generate a part of a partial class based on some class and method attributes.
Roxy is built based on the first strategy: it allows to create new types on the fly within the same program that uses it. I do plan, however, to also create custom single file generator tools and since Roxy is all Roslyn (not Reflection) based, I should be able to reuse most of its code for the VS custom plug in.
In future articles, I plan to propose C# language changes to add Roxy capabilities into the language itself (which will also be more type safe and efficient).
Roxy vs Castle
Roxy is built from a totally different perspective than Castle. It generates the code and then compiles it at runtime, not just modifies the compiled types. In that respect, it is much more powerful, but in general, spends more time on the initialization. Once the initialization is completed and the types are generated and compiled - it is as fast as any compiled code. For most applications, the extra initialization time due to Roxy initalization should be negligible in comparison to the whole application initialization time.
Also, unlike Castle, there will be a very detailed and clear documentation so that developers will not have to discover each feature by themselves.
What this Article Is and Is Not
This article presents several demo examples of Roxy usage which greatly simplified my software development experience (and hopefully will also simplify that of many other software engineers).
More articles with in-depth discussions of Roxy usage and specifically separation of concerns will be coming shortly.
The State of Roxy Project and Location of Roxy Code
Roxy is a code in progress. I plan to continue improving it while at the same time using it in my other projects (dogfooding).
At this point, Roxy cannot be called a full IoC container (only a proxy generator) but I plan to add the IoC functionality soon.
When it comes to code generation, there are several limitations at this point. The two most prominent limitations are:
- At this point, Roxy does not handle method overloading: the classes and interfaces used by Roxy should not have any overloaded methods.
- Roxy can use generic classes but it cannot produce them - the result of Roxy generation should always be concrete (with all generic arguments resolved).
Roxy is an open source project located on Github Roxy.
If you have any ideas that you would want to see as part of Roxy or find any bugs, please, open an issue on Github at Roxy Issues.
Roxy Samples
Sample Code Location and Usage
Roxy samples are also located on Github within Roxy Demos repository.
In order to run the samples, you have to install nuget NP.Roxy
package from nuget.org.
Default Implementation of Interface Sample
This is a very simple sample located under NP.Roxy.Demos.InterfaceImpl
solution.
Here is the main code:
IPerson person = Core.Concretize<IPerson>();
person.FirstName = "Joe";
person.LastName = "Doe";
person.Age = 35;
person.Profession = "Astronaut";
Console.WriteLine($"Name='{person.FirstName} {person.LastName}';
Age='{person.Age}'; Profession='{person.Profession}'");
Running the sample will result in the following string
printed to the console: "Name='Joe Doe'; Age='35'; Profession='Astronaut'"
.
Note that when you run the application - there will be an about 2-3 second delay. This delay comes mostly from:
- Roslyn initialization - this is a one-time delay
- Dynamic assembly generation and loading - this will happen any time the Roslyn project is recompiled and the assembly is reloaded.
The assembly reloading happens the first time you request a generated type or when you call RegenerateAssembly
method on the Core
. The correct strategy is to create all the types you need within the application during the application initialization and generate and load the dynamic assembly only once during the initialization stage.
Concretize
method provides the default implementation for all the properties within the interface IPerson
(which is Auto property implementation). Here is the generated Person
class:
public class Person_Concretization : NoClass, IPerson, NoInterface
{
public static Core TheCore { get; set; }
#region Default Constructor
public Person_Concretization ()
{
}
#endregion Default Constructor
#region Generated Properties
public string FirstName
{
get;
set;
}
public string LastName
{
get;
set;
}
public int Age
{
get;
set;
}
public string Profession
{
get;
set;
}
#endregion Generated Properties
}
Now take a look at the top of Program.Main
method:
Core.SetSaveOnErrorPath("GeneratedCode");
As explained in the comment, this will trigger a generated code dump on dynamic assembly compilation error.
There is also a line that causes a code dump at the bottom (in this case, the code dump occurs when the program completes successfully):
Core.Save("GeneratedCode");
Wrapper Generation
This sample NP.Rosy.Demos.Wrappers
demonstrates how to adapt a class to an interface that it does not implement. Moreover, it shows also how to expose the classes' non-public properties and methods via the adaptation to the interface.
When working with 3rd party libraries (e.g. Teleric, DevExpress or Roslyn), I often encountered that the functionality that I needed to access was not public
. I had to use C# Reflection in order to get or set the non-public values or call non-public methods. As presented below, Roxy makes accessing non-public functionality extremely easy.
Important Note: Accessing non-public functionality should not be done lightly - only if you really know what you are doing.
The IPerson
interface we want to implement is very similar to the one of the previous sample, but has a method string GetFullNameAndProfession()
on top of the properties:
public interface IPerson
{
string FirstName { get; set; }
string LastName { get; set; }
int Age { get; set; }
string Profession { get; set; }
string GetFullNameAndProfession();
}
We want to wrap (adapt) PersonImpl
class to this interface:
public class PersonImpl
{
public string FirstName { get; set; }
private string LastName { get; set; }
private int Age { get; set; }
private string TheProfession { get; set; }
private string GetFullNameAndProfession()
{
return $"{FirstName} {LastName} - {TheProfession}";
}
}
Note that all the members of the class are private
aside from FirstName
property. I did not want to create another project to demonstrate adapting non-public 3rd party functionality, so instead I created the type to adapt within the same project, but made most of its members private
.
Also note that all its property and method names match the corresponding member names of IPerson
interface aside from TheProfession
property (whose counterpart within IPerson
interface is called Profession
without prefix "The
").
Here is the main part of Program.Main(...)
method code:
#region create the generated type configuration object
ITypeConfig typeConfig =
Core.FindOrCreateTypeConfig<IPerson, PersonImplementationWrapperInterface>("MyPersonImplementation");
typeConfig.SetAllowNonPublicForAllMembers
(nameof(PersonImplementationWrapperInterface.ThePersonImplementation));
typeConfig.SetMemberMap
(
nameof(PersonImplementationWrapperInterface.ThePersonImplementation),
"TheProfession",
nameof(IPerson.Profession)
);
typeConfig.ConfigurationCompleted();
#endregion create the generated type configuration object
IPerson person =
Core.GetInstanceOfGeneratedType<IPerson>("MyPersonImplementation");
person.FirstName = "Joe";
person.LastName = "Doe";
person.Age = 35;
person.Profession = "Astronaut";
Console.WriteLine($"Name/Profession='{person.GetFullNameAndProfession()}'; Age='{person.Age}'");
Let's take a look at the top of the method (the part within "create the generated type configuration object" region).
Line...
ITypeConfig typeConfig =
Core.FindOrCreateTypeConfig<IPerson,
PersonImplementationWrapperInterface>("MyPersonImplementation");
...creates the generated type configuration object. This object will be responsible for generating the adaptor class named "MyPersonImplementation
").
Take a look at the Type
arguments to the Core.FindOrCreateTypeConfig
method. The first argument specifies the interface
we want to implement. The second argument PersonImplementationWrapperInterface
is more complex and should be explained in detail.
PersonImplementationWrapperInterface
interface is defined at the top of the Program.cs file:
public interface PersonImplementationWrapperInterface
{
PersonImpl ThePersonImplementation { get; }
}
It only contains one getter-only property ThePersonImplementation
. This property is of the type we want to wrap.
The reason I introduced such interfaces for the wrapped classes is because Roxy has power to wrap multiple objects (not only a single one as in this simple sample). Moreover, several of the wrapped classes can be of the same type (their members can be mapped to different properties of the interface we implement) so that the type might not uniquely identify such wrapped class. Therefore, I introduced this very simple wrapper interface that lists all the wrapped objects under different names. These more complex cases will be discussed in the future articles.
Line...
typeConfig.SetAllowNonPublicForAllMembers
(
nameof(PersonImplementationWrapperInterface.ThePersonImplementation)
);
...states that all the members of the wrapped object should be accessible even if they are not public
.
Statement...
typeConfig.SetMemberMap
(
nameof(PersonImplementationWrapperInterface.ThePersonImplementation),
"TheProfession",
nameof(IPerson.Profession)
);
...maps the TheProfession
property of the wrapped object into Profession
property of the interface
.
Note that if not for the Profession
property name mismatch, we could have used the following shortcut method instead of the whole block within "create the generated type configuration object" region:
IPerson person =
Core.CreateWrapperWithNonPublicMembers<IPerson, PersonImplementationWrapperInterface>
("MyPersonImplementation");
Running the code will result in the following line printed to console: "Name/Profession='Joe Doe - Astronaut'; Age='35'"
Mapping an Enumeration into an Interface
This sample is located under NP.Roxy.Demos.EnumToInterface
solution. The Program.Main
code is very simple:
Core.CreateEnumerationAdapter<IProduct, ProductKind>(typeof(ProductKindExtensions));
IProduct product =
Core.CreateEnumWrapper<IProduct, ProductKind>(ProductKind.FinancialInstrument);
Console.WriteLine($"product: {product.GetDisplayName()}; Description: {product.GetDescription()}");
ProductKind
is an enumeration that has a static
extension class ProductKindExtensions
:
public enum ProductKind
{
Grocery,
FinancialInstrument,
Information
}
public static class ProductKindExtensions
{
public static string GetDisplayName(this ProductKind productKind)
{
switch(productKind)
{
case ProductKind.Grocery:
return "Grocery";
case ProductKind.FinancialInstrument:
return "Financial Instrument";
case ProductKind.Information:
return "Information";
}
return null;
}
private static string GetDescription(this ProductKind productKind)
{
switch (productKind)
{
case ProductKind.Grocery:
return "Products you can buy in a grocery store";
case ProductKind.FinancialInstrument:
return "Products you can buy on a stock exchange";
case ProductKind.Information:
return "Products you can get on the Internet";
}
return null;
}
}
Interface IProduct
has only two methods:
public interface IProduct
{
string GetDisplayName();
string GetDescription();
}
Note that the names of the IProduct
methods match those of the ProductKindExtensions
class. If this was not the case, we would have to do some extra work mapping the names.
Also note that one of the extension methods (namely ProductKindExtensions.GetDescription(...)
is private
. I made it private
on purpose to show that non-public extension methods (e.g. internal methods from a 3rd party library) can still be wrapped with Roxy.
Summary
Here, I introduced a new Roslyn based run time code generation Roxy package which I plan to evolve into a full blown IoC container.
This article previews some features which can greatly empower software developers.
More complex features and abilities of Roxy will be discussed in future articles. In particular, the following features will be presented:
- Separation of Concerns with Roxy
- Events implementation
- Multiple wrapped (adapted) classes
- SuperClasses instead and together with interfaces
- Smart Mixins
History
Removed second license - set correct license (Apache) 1/29/2018