4

I am currently developing an API that is compiled dynamically to an assembly based up some predefined rules stored in XML documents.

I am having a hard time trying to get CodeDOM to generate properties with a public getter and private setter decorated with custom attributes.

This is what I am after:

[Conditional()]
public E3477 E3477 { get; private set; }

But I am getting this, which is no good as I don't want the setter exposed publically:

[Conditional()]
public E3477 E3477 
{
    get
    {
    }
    set
    {
    }
}

This is the code I am using:

var componentRef = string.Format( "E{0}", component.XPathSelectElement( "Element" ).Value );
CodeMemberProperty prop = new CodeMemberProperty();
prop.Name = componentRef;
prop.Type = new CodeTypeReference( componentRef );
prop.HasSet = true;
prop.HasGet = true;
prop.Attributes = MemberAttributes.Public;
CodeAttributeDeclaration conditionalAttr = new CodeAttributeDeclaration( "Conditional" );
prop.CustomAttributes.Add( conditionalAttr );

compositeElementClass.Members.Add( prop );

Is what I am after even possible in CodeDOM?

I was told by the community on Stack Overflow to use CodeDom and create the assemblies rather than using MSBuild, which I tried to do originally but couldn't get it to work.

** EDITED with hard to read code to see if it can be simplified **

string GenerateDataElementsCode()
{
    CodeNamespace globalNamespace = new CodeNamespace();
    globalNamespace.Imports.Add( new CodeNamespaceImport( string.Format( "{0}.DataElements", _project.Target.RootNamespace ) ) );

    CodeCompileUnit unit = new CodeCompileUnit();

    unit.Namespaces.Add( globalNamespace );

    CodeNamespace ns = new CodeNamespace( string.Format( "{0}.DataElements", _project.Target.RootNamespace ) );

    var codesDoc = XDocument.Load( string.Format( @"{0}\{1}", _project.Source.RootPath, _project.Source.UNCL ) );

    var doc = XDocument.Load( string.Format( @"{0}\{1}", _project.Source.RootPath, _project.Source.EDED ) );
    foreach ( XNode node in doc.Descendants( "DataElement" ) )
    {
        CodeTypeDeclaration dataElementClass = new CodeTypeDeclaration()
        {
            Name = string.Format( "E{0}", node.XPathSelectElement( "Identifier" ).Value ),
            IsClass = true
        };

        dataElementClass.Comments.Add( new CodeCommentStatement( node.XPathSelectElement( "Description" ).Value, true ) );

        dataElementClass.BaseTypes.Add( "SimpleObject" );

        CodeAttributeDeclaration dataElementAttr = new CodeAttributeDeclaration( "DataElement" );
        dataElementAttr.Arguments.Add(
            new CodeAttributeArgument
                    {
                        Name = "",
                        Value = new CodePrimitiveExpression( node.XPathSelectElement( "Identifier" ).Value )
                    } );
        dataElementAttr.Arguments.Add(
            new CodeAttributeArgument
            {
                Name = "",
                Value = new CodePrimitiveExpression( node.XPathSelectElement( "Name" ).Value )
            } );
        dataElementAttr.Arguments.Add(
            new CodeAttributeArgument
            {
                Name = "",
                Value = new CodePrimitiveExpression( node.XPathSelectElement( "Description" ).Value )
            } );

        CodeAttributeDeclaration dataElementFormatAttr = new CodeAttributeDeclaration( "DataElementFormat" );
        dataElementFormatAttr.Arguments.Add(
            new CodeAttributeArgument
            {
                Name = "Cardinality",
                Value = new CodePrimitiveExpression( node.XPathSelectElement( "Cardinality" ).Value )
            } );

        dataElementClass.CustomAttributes.Add( dataElementAttr );
        dataElementClass.CustomAttributes.Add( dataElementFormatAttr );

        var codes = codesDoc.XPathSelectElements( "SimpleDataElements/SimpleDataElement/CodeLists/CodeList" ).Where( a => a.XPathSelectElement( "../../Code" ).Value == node.XPathSelectElement( "Identifier" ).Value );

        if ( codes.Count() > 0 )
        {

            CodeTypeDeclaration codesClass = new CodeTypeDeclaration( "Codes" );
            codesClass.Attributes = MemberAttributes.Static;
            codesClass.IsClass = true;

            foreach ( XNode codeNode in codes )
            {
                CodeMemberField con = new CodeMemberField( typeof( string ), string.Format( "Code{0}", codeNode.XPathSelectElement( "Code" ).Value ) );
                con.Attributes = MemberAttributes.Public | MemberAttributes.Const;
                con.InitExpression = new CodePrimitiveExpression( codeNode.XPathSelectElement( "Code" ).Value );

                con.Comments.Add( new CodeCommentStatement( codeNode.XPathSelectElement( "Description" ).Value, true ) );

                codesClass.Members.Add( con );
            }

            dataElementClass.Members.Add( codesClass );

        }

        ns.Types.Add( dataElementClass );

    }

    unit.Namespaces.Add( ns );

    var provider = new Microsoft.CSharp.CSharpCodeProvider();

    using ( var sourceCode = new StringWriter() )
    using ( var indentedTextWriter = new IndentedTextWriter( sourceCode, "    " ) )
    {
        // Generate source code using the code provider.
        provider.GenerateCodeFromCompileUnit( unit,
            indentedTextWriter,
            new CodeGeneratorOptions() { BracingStyle = "C" } );

        return sourceCode.GetStringBuilder().ToString();
    }
}
Intrepid
  • 2,781
  • 2
  • 29
  • 54
  • Have you considered using simply `CSharpCodeProvider`, like in [my answer](http://stackoverflow.com/questions/19932086/reflection-emit-building-a-entity-graph/19941406#19941406). It will allow you to build string with desired C# source code and simply compile it dynamically. – Konrad Kokosa Jul 04 '14 at 14:18
  • @KonradKokosa Yes did did, but it got too messy as I am injecting values from XML documents and it was getting messy and difficult to follow. – Intrepid Jul 04 '14 at 14:21

3 Answers3

1

As others have pointed out, private setters aren't natively supported by CodeDom (IIRC because it's not a concept that applies to all languages served by CodeDom).

Most solutions recommend explicitly hardcoding "{ get; private set; }" into a snippet expression, but this doesn't really help if you need to have accessors with logic. This is a related but different approach that let me solve a similar issue. It's not pretty, but does the trick:

Given some helper class along the lines of

public static class SnippetGenerator
{
    private static CodeDomProvider codeGenProvider = CodeDomProvider.CreateProvider("CSharp");
    private static CodeGeneratorOptions codeGenOptions = new CodeGeneratorOptions() { BracingStyle = "C", BlankLinesBetweenMembers = false };
    private static StringWriter stringWriter = new StringWriter();

    public static string GenerateSnippet(CodeTypeMember member)
    {
        codeGenProvider.GenerateCodeFromMember(member, stringWriter, codeGenOptions);
        string snippet = stringWriter.ToString();
        stringWriter.GetStringBuilder().Length = 0;
        return snippet;
    }
}

You can then convert existing CodeDom properties into snippets that you can post-process with string manipulation:

CodeMemberProperty componentProperty = new CodeMemberProperty();
... //(build out your property)

// inject private scope onto setter
string propertySnippet = SnippetGenerator.GenerateSnippet(componentProperty);
propertySnippet = propertySnippet.Replace("  set\r\n", "  private set\r\n");
CodeSnippetTypeMember propertySnippetMember = new CodeSnippetTypeMember(propertySnippet);

Note: There's likely a better way of sniping the set than including the "\r\n" (which could possibly be different depending on your generation/platform settings) but it was an easy way to make sure it wasn't grabbing any incorrect substrings. Choose whatever way best fits your project.

Leoul
  • 31
  • 3
0

When someone say CodeDOM, I see it this way:

// create compiler
CodeDomProvider provider = CSharpCodeProvider.CreateProvider("C#");
CompilerParameters options = new CompilerParameters();
// add all loaded assemblies
options.ReferencedAssemblies.AddRange(
    AppDomain.CurrentDomain.GetAssemblies().Where(item => !item.IsDynamic).Select(item => item.Location).ToArray());
options.GenerateExecutable = false;
options.GenerateInMemory = true;
// source
string source = "using System;namespace Test{public class Test{";
source += "[Conditional()]public E3477 E3477 { get; private set; }";
source += ...
// compile
CompilerResults result = provider.CompileAssemblyFromSource(options, source);
svick
  • 236,525
  • 50
  • 385
  • 514
Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • I am currently adding the source in a similar way to what you have shown, but I am generating the classes using `CodeDOM` objects, and then converting the code to a string before passing it to the compiler. I tried doing it the same was as you did, but it got too messy with values being injected into strings making the final code hard to follow. – Intrepid Jul 04 '14 at 14:20
  • @MikeClarke, "generating the classes using CodeDOM, and then converting the code to a string before passing it to the compiler"?! Sounds very over-complicated... – Konrad Kokosa Jul 04 '14 at 14:23
  • @KonradKokosa I used a tutorial to do this; that's why I went down that route. – Intrepid Jul 04 '14 at 14:26
  • Source as `string` is more *readable* than your code. If it gets too messy (many `string.Format` ?) then simply organize it, to example, with the use of extension methods, which could modify given string as template and produce wanted result. It would be nice to see an example of `final code hard to follow`, maybe it's easy to make it more simple. – Sinatr Jul 04 '14 at 14:27
  • @Sinatr I've amended the question to include the hard to read code. – Intrepid Jul 04 '14 at 14:31
  • Do you need nice looking *source* (generated by `GenerateCodeFromCompileUnit`) or do you need compiled assembly? – Sinatr Jul 04 '14 at 14:40
  • @Sinatr I want both because I want to see the final code generated. For a release build I will not be using `GenerateCodeFromCompileUnit()`. – Intrepid Jul 04 '14 at 14:42
  • Correct me if I am wrong at what you are doing: read existing source, parse `xml` with rules, generate `Codes` class, add it to source code once (once?), save it. – Sinatr Jul 04 '14 at 14:54
-1

Well, if you want to use Codedom Objects, you could create a codememberfield and add a getter/setter to its name.

CodeMemberfield field = new CodeMemberField
  {
      Name = "YourPropertyName",
      Attributes = MemberAttributes.Public | MemberAttributes.Final,
      Type = new CodeTypeReference(typeof(YourClassName)),
  };

field.Name += " { get; private set; }";

See https://stackoverflow.com/a/13679314/3496840

Community
  • 1
  • 1
Shadow3097
  • 28
  • 7