Click here to Skip to main content
14,211,360 members
Click here to Skip to main content

Introducing Roxy: Powerful Proxy Generation and IoC Container Package

,
Rate this:
4.57 (7 votes)
Please Sign up or sign in to vote.
4.57 (7 votes)
29 Jan 2018     Apache     7.6K   44   6   7
Roxy is a powerful new Roslyn based proxy generation packages that facilitates separation of concerns, creating adaptors and smart mixins.

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:

  1. Converting an interface into a class using built-in conversion or easily created custom conversion mechanisms.
  2. Creating adaptors that adapt multiple classes to an interface or an abstract class or both.
  3. Achieving a greater separation of concerns by mixing behaviors with the objects that they modify.
  4. Easily accessing non-public properties and methods of 3rd party components.
  5. 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:

  1. Resolving interfaces and abstract classes to concrete pre-specified (or generated) types
  2. Producing singleton objects
  3. 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:

  1. Allowing to generate generic (not fully resolved) classes for generic interfaces
  2. 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:

  1. At this point, Roxy does not handle method overloading: the classes and interfaces used by Roxy should not have any overloaded methods.
  2. 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:

// get default implementation of IPerson
// interface containing only propertys
// the default property implementation
// is the auto property
IPerson person = Core.Concretize<IPerson>();

person.FirstName = "Joe";

person.LastName = "Doe";

person.Age = 35;

person.Profession = "Astronaut";

// test that the properties have indeed been assigned. 
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:

  1. Roslyn initialization - this is a one-time delay
  2. 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:

// if there is a compiler error
// all the generated code will be dumped
// into "GeneratedCode" folder located within 
// the directory containing the executable
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):

// dump all the generated code into 
// "GeneratedCode" folder located within 
// the directory containing the executable
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
// get the type configuration object. The class that it is going to generate
// will be called "MyPersonImplementation"
ITypeConfig typeConfig =
    Core.FindOrCreateTypeConfig<IPerson, PersonImplementationWrapperInterface>("MyPersonImplementation");

// allow access to non-public members of 
// PersonImplementationWrapperInterface.ThePersonImplementation object.
typeConfig.SetAllowNonPublicForAllMembers
(nameof(PersonImplementationWrapperInterface.ThePersonImplementation));

// map TheProfession property of the wrapped object
// into Profession property of the IPerson interface.
typeConfig.SetMemberMap
(
    nameof(PersonImplementationWrapperInterface.ThePersonImplementation),
    "TheProfession",
    nameof(IPerson.Profession)
);

// Signal that the configuration is completed, 
// after ConfigurationCompleted() method is called
// TypeConfig object for this class cannot be modified.
typeConfig.ConfigurationCompleted();
#endregion create the generated type configuration object

// get the instance of the generated type "MyPersonImplementation"
IPerson person = 
    Core.GetInstanceOfGeneratedType<IPerson>("MyPersonImplementation");


// set the properties
person.FirstName = "Joe";

person.LastName = "Doe";

person.Age = 35;

person.Profession = "Astronaut";

// test that the wrapped properties and the method work
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:

// we create an adaptor adapting ProductKind enumeration
// to IProduct interface using extension methods from the static 
// ProductKindExtensions class
Core.CreateEnumerationAdapter<IProduct, ProductKind>(typeof(ProductKindExtensions));

// enumeration value ProductKind.FinancialInstrument is converted into
// IProduct interface
IProduct product =
    Core.CreateEnumWrapper<IProduct, ProductKind>(ProductKind.FinancialInstrument);

// we test the methods of the resulting object that implements IProduct interface.
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
{
    // returns a displayable short name for the ProductKind
    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;
    }

    // returns the full description of the ProductKind
    // note that the method is private
    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:

  1. Separation of Concerns with Roxy
  2. Events implementation
  3. Multiple wrapped (adapted) classes
  4. SuperClasses instead and together with interfaces
  5. Smart Mixins

History

Removed second license - set correct license (Apache) 1/29/2018

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0

Share

About the Author

Nick Polyak
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I have my Ph.D. from RPI.

here is my linkedin profile - I'll be happy to connect!

Comments and Discussions

 
Questionclearly a lot of work went into this Pin
Sacha Barber28-Jan-18 22:24
mvaSacha Barber28-Jan-18 22:24 
AnswerRe: clearly a lot of work went into this Pin
Nick Polyak29-Jan-18 0:04
mvaNick Polyak29-Jan-18 0:04 
GeneralWrapping private methods is dangerous Pin
Klaus Luedenscheidt27-Jan-18 18:14
memberKlaus Luedenscheidt27-Jan-18 18:14 
GeneralRe: Wrapping private methods is dangerous Pin
Nick Polyak27-Jan-18 19:38
mvaNick Polyak27-Jan-18 19:38 
GeneralRe: Wrapping private methods is dangerous Pin
Nick Polyak27-Jan-18 20:05
mvaNick Polyak27-Jan-18 20:05 
GeneralRe: Wrapping private methods is dangerous Pin
Klaus Luedenscheidt28-Jan-18 18:21
memberKlaus Luedenscheidt28-Jan-18 18:21 
GeneralRe: Wrapping private methods is dangerous Pin
Nick Polyak28-Jan-18 19:01
mvaNick Polyak28-Jan-18 19:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Layout: fixed | fluid
Posted 26 Jan 2018

Article Copyright 2018 by Nick Polyak
Everything else Copyright © CodeProject, 1999-2019

Server Web01
Version 2.8.190619.1