Dynamic Invocation of WCF Service Using Reflection - c#

I'm having a problem passing a generic collection to a WCF service method when invoked using reflection. Specifically, the collection is of type List<KeyValuePair<string,string>>.
My goal is to be able to execute methods of a WCF service dynamically during runtime without adding any references to the service whatsoever in my client app. A user should be able to add a service during runtime and the app should just magically be able to handle it.
Service Interface
[ServiceContract]
public interface ITestService
{
[OperationContract]
string ProcessSimpleType(string value);
[OperationContract]
string ProcessGenericCollection(List<KeyValuePair<string, string>> genericCol);
}
Service Implementation
public class TestService : ITestService
{
public string ProcessSimpleType(string value)
{
return value;
}
public string ProcessGenericCollection(List<KeyValuePair<string, string>> genericCol)
{
return "Hello World!";
}
}
Client Code
try
{
Uri mexAddress = new Uri("http://localhost:8732/TestService/?wsdl");
MetadataExchangeClientMode mexMode = MetadataExchangeClientMode.HttpGet;
string contractName = "ITestService";
string operationName = "ProcessGenericCollection";
List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>();
list.Add(new KeyValuePair<string, string>("key", "value"));
object[] operationParameters = new object[] { list };
MetadataExchangeClient mexClient = new MetadataExchangeClient(mexAddress, mexMode);
mexClient.ResolveMetadataReferences = true;
MetadataSet metaSet = mexClient.GetMetadata();
WsdlImporter importer = new WsdlImporter(metaSet);
Collection<ContractDescription> contracts = importer.ImportAllContracts();
ServiceEndpointCollection allEndpoints = importer.ImportAllEndpoints();
ServiceContractGenerator generator = new ServiceContractGenerator();
var endpointsForContracts = new Dictionary<string, IEnumerable<ServiceEndpoint>>();
foreach (ContractDescription contract in contracts)
{
generator.GenerateServiceContractType(contract);
endpointsForContracts[contract.Name] = allEndpoints.Where(
se => se.Contract.Name == contract.Name).ToList();
}
if (generator.Errors.Count != 0)
throw new Exception("There were errors during code compilation.");
CodeGeneratorOptions options = new CodeGeneratorOptions();
options.BracingStyle = "C";
CodeDomProvider codeDomProvider = CodeDomProvider.CreateProvider("C#");
CompilerParameters compilerParameters = new CompilerParameters(
new string[] {
"System.dll", "System.ServiceModel.dll",
"System.Runtime.Serialization.dll" });
compilerParameters.GenerateInMemory = true;
CompilerResults results = codeDomProvider.CompileAssemblyFromDom(
compilerParameters, generator.TargetCompileUnit);
if (results.Errors.Count > 0)
{
throw new Exception("There were errors during generated code compilation");
}
else
{
Type clientProxyType = results.CompiledAssembly.GetTypes().First(
t => t.IsClass &&
t.GetInterface(contractName) != null &&
t.GetInterface(typeof(ICommunicationObject).Name) != null);
ServiceEndpoint se = endpointsForContracts[contractName].First();
object instance = results.CompiledAssembly.CreateInstance(
clientProxyType.Name,
false,
System.Reflection.BindingFlags.CreateInstance,
null,
new object[] { se.Binding, se.Address },
CultureInfo.CurrentCulture, null);
var methodInfo = instance.GetType().GetMethod(operationName);
//Invoking the ProcessGenericCollection via reflection will throw an exception
object retVal = methodInfo.Invoke(instance, BindingFlags.InvokeMethod, null, operationParameters, null);
Console.WriteLine(retVal.ToString());
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
The error that is thrown is:
{"Object of type
'System.Collections.Generic.List1[System.Collections.Generic.KeyValuePair2[System.String,System.String]]'
cannot be converted to type
'System.Collections.Generic.KeyValuePairOfstringstring[]'."}
Keep in mind, this works gloriously when testing against the ProcessSimpleType(...) method and passing in a simple type. My issue is only with ProcessGenericCollection(...). Has anyone ever encountered this issue, and if so, how did you overcome it?

Thanks to a colleague for providing the solution. For those of you with a similar issue, I inserted the following:
...
...
WsdlImporter importer = new WsdlImporter(metaSet);
//BEGIN INSERT
XsdDataContractImporter xsd = new XsdDataContractImporter();
xsd.Options = new ImportOptions();
xsd.Options.ImportXmlType = true;
xsd.Options.GenerateSerializable = true;
xsd.Options.ReferencedTypes.Add(typeof(KeyValuePair<string, string>));
xsd.Options.ReferencedTypes.Add(typeof(System.Collections.Generic.List<KeyValuePair<string, string>>));
importer.State.Add(typeof(XsdDataContractImporter), xsd);
//END INSERT
Collection<ContractDescription> contracts = importer.ImportAllContracts();
...
...

Related

System.PlatformNotSupportedException Compiling C# code at runtime .NET Core

Trying to compile simple C# code at runtime on .NET Core but have this error:
System.PlatformNotSupportedException: 'Operation is not supported on
this platform.'
on this line:
CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
My code:
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Text;
string code = #"
using System;
namespace First
{
public class Program
{
public static void Main()
{
" +
"Console.WriteLine(\"Hello, world!\");"
+ #"
}
}
}
";
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters parameters = new CompilerParameters();
parameters.ReferencedAssemblies.Add("System.Drawing.dll");
parameters.GenerateInMemory = true;
parameters.GenerateExecutable = true;
CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
if (results.Errors.HasErrors)
{
StringBuilder sb = new StringBuilder();
foreach (CompilerError error in results.Errors)
{
sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText));
}
throw new InvalidOperationException(sb.ToString());
}
Assembly assembly = results.CompiledAssembly;
Type program = assembly.GetType("First.Program");
MethodInfo main = program.GetMethod("Main");
main.Invoke(null, null);
I recommend using the Roslyn compiler. You'll need to add references Microsoft.CodeAnalysis and Microsoft.CodeAnalysis.CSharp for the following example to work. Note, that the RoslynCompiler class loads the assembly dynamically. You can modify the class fairly easily to use a FileStream instead of a MemoryStream if you want to save the compilation to disk for reuse.
Sample Usage of RoslynCompiler Class (below)
string code = #"
using System;
namespace First
{
public class Program
{
public static void Main()
{
Console.WriteLine(\"Hello, world!\");
}
public static void WithParams(string message)
{
Console.WriteLine(message);
}
}
}
";
var compiler = new RoslynCompiler("First.Program", code, new[] {typeof(Console)});
var type = compiler.Compile();
type.GetMethod("Main").Invoke(null, null);
//result: Hellow World!
// pass an object array to the second null parameter to pass arguments
type.GetMethod("WithParams").Invoke(null, new object[] {"Hi there from invoke!"});
//result: Hi from invoke
Roslyn Compiler Class (Quick and Dirty Example)
public class RoslynCompiler
{
readonly CSharpCompilation _compilation;
Assembly _generatedAssembly;
Type? _proxyType;
string _assemblyName;
string _typeName;
public RoslynCompiler(string typeName, string code, Type[] typesToReference)
{
_typeName = typeName;
var refs = typesToReference.Select(h => MetadataReference.CreateFromFile(h.Assembly.Location) as MetadataReference).ToList();
//some default refeerences
refs.Add(MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly.Location), "System.Runtime.dll")));
refs.Add(MetadataReference.CreateFromFile(typeof(Object).Assembly.Location));
//generate syntax tree from code and config compilation options
var syntax = CSharpSyntaxTree.ParseText(code);
var options = new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
allowUnsafe: true,
optimizationLevel: OptimizationLevel.Release);
_compilation = CSharpCompilation.Create(_assemblyName = Guid.NewGuid().ToString(), new List<SyntaxTree> { syntax }, refs, options);
}
public Type Compile()
{
if (_proxyType != null) return _proxyType;
using (var ms = new MemoryStream())
{
var result = _compilation.Emit(ms);
if (!result.Success)
{
var compilationErrors = result.Diagnostics.Where(diagnostic =>
diagnostic.IsWarningAsError ||
diagnostic.Severity == DiagnosticSeverity.Error)
.ToList();
if (compilationErrors.Any())
{
var firstError = compilationErrors.First();
var errorNumber = firstError.Id;
var errorDescription = firstError.GetMessage();
var firstErrorMessage = $"{errorNumber}: {errorDescription};";
var exception = new Exception($"Compilation failed, first error is: {firstErrorMessage}");
compilationErrors.ForEach(e => { if (!exception.Data.Contains(e.Id)) exception.Data.Add(e.Id, e.GetMessage()); });
throw exception;
}
}
ms.Seek(0, SeekOrigin.Begin);
_generatedAssembly = AssemblyLoadContext.Default.LoadFromStream(ms);
_proxyType = _generatedAssembly.GetType(_typeName);
return _proxyType;
}
}
}
Performance Tip
If performance matters, use delegates as opposed to Invoke as follows to achieve near pre-compiled throughput:
void Main()
{
string code = #"OMITTED EXAMPLE CODE FROM SAMPLE ABOVE";
var compiler = new RoslynCompiler("First.Program", code, new[] { typeof(Console) });
var type = compiler.Compile();
// If perf matters used delegates to get near pre-compiled througput vs Invoke()
var cachedDelegate = new DynamicDelegateCacheExample(type);
cachedDelegate.Main();
//result: Hellow world!
cachedDelegate.Main("Hi there from cached delegate!");
//result: Hi there from cached delegate!
}
public class DynamicDelegateCacheExample
{
delegate void methodNoParams();
delegate void methodWithParamas(string message);
private static methodNoParams cachedDelegate;
private static methodWithParamas cachedDelegateWeithParams;
public DynamicDelegateCacheExample(Type myDynamicType)
{
cachedDelegate = myDynamicType.GetMethod("Main").CreateDelegate<methodNoParams>();
cachedDelegateWeithParams = myDynamicType.GetMethod("WithParams").CreateDelegate<methodWithParamas>();
}
public void Main() => cachedDelegate();
public void Main(string message) => cachedDelegateWeithParams(message);
}
With .net core netstandard and publishing to a self contained exe there are a couple more tricks you'll need;
public static ModuleMetadata GetMetadata(this Assembly assembly)
{
// based on https://github.com/dotnet/runtime/issues/36590#issuecomment-689883856
unsafe
{
return assembly.TryGetRawMetadata(out var blob, out var len)
? ModuleMetadata.CreateFromMetadata((IntPtr)blob, len)
: throw new InvalidOperationException($"Could not get metadata from {assembly.FullName}");
}
}
#pragma warning disable IL3000
public static MetadataReference GetReference(this Assembly assembly)
=> (assembly.Location == "")
? AssemblyMetadata.Create(assembly.GetMetadata()).GetReference()
: MetadataReference.CreateFromFile(assembly.Location);
#pragma warning restore IL3000
public static Assembly Compile(string source, IEnumerable<Type> references)
{
var refs = new HashSet<Assembly>(){
typeof(object).Assembly
};
foreach (var t in references)
refs.Add(t.Assembly);
foreach (var a in AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic
&& a.ExportedTypes.Count() == 0
&& (a.FullName.Contains("netstandard") || a.FullName.Contains("System.Runtime,"))))
refs.Add(a);
var options = CSharpParseOptions.Default
.WithLanguageVersion(LanguageVersion.Latest);
var compileOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default);
var compilation = CSharpCompilation.Create("Dynamic",
new[] { SyntaxFactory.ParseSyntaxTree(source, options) },
refs.Select(a => a.GetReference()),
compileOptions
);
using var ms = new MemoryStream();
var e = compilation.Emit(ms);
if (!e.Success)
throw new Exception("Compilation failed");
ms.Seek(0, SeekOrigin.Begin);
var context = new AssemblyLoadContext(null, true);
return context.LoadFromStream(ms);
}
// for dynamically implementing some interface;
public static C CompileInstance<C>(string source, IEnumerable<Type> references)
{
var assembly = Compile(source, references);
var modelType = assembly.DefinedTypes.Where(t => typeof(C).IsAssignableFrom(t)).Single();
return (C)Activator.CreateInstance(modelType);
}

Argument 2: Cannot convert from Expression<System.Func<T, bool>> to "System.func<T, bool>"

I am implementing MongoDBRepository using this code as a reference but it is giving me an error.
Here's my sample code.
public static IMongoCollection<Book> Initialize()
{
MongoClientSettings settings = new MongoClientSettings();
settings.Server = new MongoServerAddress(host, 10255);
settings.UseSsl = true;
settings.SslSettings = new SslSettings();
settings.SslSettings.EnabledSslProtocols = SslProtocols.Tls12;
MongoIdentity identity = new MongoInternalIdentity(dbName, userName);
MongoIdentityEvidence evidence = new PasswordEvidence(password);
settings.Credentials = new List<MongoCredential>()
{
new MongoCredential("SCRAM-SHA-1", identity, evidence)
};
MongoClient client = new MongoClient(settings);
var database = client.GetDatabase(dbName);
var bookCollection = database.GetCollection<Book>(collectionName);
return bookCollection;
}
public Task<IEnumerable<Models.Book>> GetBooksAsync(Expression<Func<Models.Book, bool>> predicate)
{
if (predicate == null) { throw new ArgumentNullException("Predicate is Null"); }
var client = Initialize();
return client.AsQueryable().Where(predicate);
}
It shows red underlines under the predicate in the last line of code. The error message shows:
Argument 2: cannot convert from 'System.Linq.Expressions.Expression<System.Func<BookProfile.Models.Book, bool>>' to 'System.Func<Book, bool>'
Could someone explain what's the cause of this problem?

Class generation from XSD during runtime

How do I generate a C# class from XSD at runtime?
Additionally, how do it list the properties that are contained by the type?
It definitely is possible.. and not too complicated. You just have to add some differen techniques.
You can use the "Description Importer" to import Service descriptions in runtime. Link
What I did was basically creating following steps:
1) Get the WSDL file with a reader (locally or remote, different approaches)
XmlTextReader myXmlReader;
myWebService = new WebServiceImporterCompiler(WSDLPath, soapVersion);
if (useLocalWSDL)
{
FileWebRequest wr = (FileWebRequest)FileWebRequest.Create(WSDLPath);
FileWebResponse wres = (FileWebResponse)wr.GetResponse();
myXmlReader = new XmlTextReader(wres.GetResponseStream());
}
else
{
Uri uri = new Uri(WSDLPath); //WEBSERVICE URI
HttpWebRequest wr = (HttpWebRequest)HttpWebRequest.Create(uri.OriginalString + "?wsdl");
wr.Credentials = wr.Credentials = new NetworkCredential(userName, password ?? "");
HttpWebResponse wres = (HttpWebResponse)wr.GetResponse();
myXmlReader = new XmlTextReader(wres.GetResponseStream());
}
2) Build an assembly from the definition / myXmlReader
Check if the xml is readable
if (!System.Web.Services.Description.ServiceDescription.CanRead(myXmlReader))
{
throw new IOException("WSDL not readable");
}
Load importer with some basic options (you might add / change something here)
I create an assembly (dll) but with the switch parameters.GenerateInMemory you will be able to generate an in memory class.
ServiceDescriptionImporter descriptionImporter = new ServiceDescriptionImporter();
ServiceDescription serviceDescription = ServiceDescription.Read(myXmlReader);
descriptionImporter.ProtocolName = soapVersion.ToString(); // EITHER SOAP OR SOAP12
descriptionImporter.AddServiceDescription(serviceDescription, null, null);
descriptionImporter.Style = ServiceDescriptionImportStyle.Client;
descriptionImporter.CodeGenerationOptions = System.Xml.Serialization.CodeGenerationOptions.GenerateProperties;
Compile assembly with CodeDomProvider
CodeCompileUnit codeUnit = new CodeCompileUnit();
CodeNamespace codeNamespace = new CodeNamespace();
codeUnit.Namespaces.Add(codeNamespace); // Add additional Namespaces
ServiceDescriptionImportWarnings importWarnings = descriptionImporter.Import(codeNamespace, codeUnit);
if (importWarnings == 0)
{
using (CodeDomProvider compiler = CodeDomProvider.CreateProvider("CSharp"))
{
string[] references = { "System.dll", "System.Web.Services.dll", "System.Xml.dll" };
CompilerParameters parameters = new CompilerParameters(references);
parameters.GenerateExecutable = false;
parameters.GenerateInMemory = false;
parameters.IncludeDebugInformation = false;
parameters.CompilerOptions = "/optimize";
parameters.TempFiles = new TempFileCollection(System.IO.Path.GetTempPath() + "xxx", false);
parameters.ReferencedAssemblies.Add("System.dll");
results = compiler.CompileAssemblyFromSource(parameters, CSharpCode);
foreach (CompilerError cError in results.Errors)
{
// log errors
}
if (results.Errors.Count > 0 || results.CompiledAssembly == null) throw new Exception("Kompilierfehler bei Assemblyerstellung");
}
}
3) Use the generated asssembly object to invoke methods for example for calling the service
public T InvokeMethod <T>(Assembly assembly, string serviceNameToCall, MethodInfo methodToCall)
{
SoapHttpClientProtocol mySoapProtocoll;
try
{
object serviceInstance = myAssembly.CreateInstance(serviceNameToCall);
mySoapProtocoll = (SoapHttpClientProtocol)serviceInstance;
mySoapProtocoll.Credentials = CredentialCache.DefaultCredentials; // or use your own
object myObject = (T)ServiceType.InvokeMember(methodToCall, BindingFlags.InvokeMethod, null, mySoapProtocoll, args);
}
}
To get the methodInfo object / available methods use reflections to iterate over the assembly / classes.
A complete guide on reflections can be found here

WF 4.5 WCF Workflow Service Application calling with REST

Want a way to call a WCF Workflow Service Application using REST instead of SOAP. I've defined a REST endpoint with custom webhttpbehavior and from that call I am trying to load the XAMLX and run it.
My first attempt fails with
Expression Activity type 'CSharpReference`1' requires compilation in order to run. Please ensure that the workflow has been compiled
I​ found code to compile the Expression before invoking the workflow and then I get errors like.
The type or namespace name 'Activities' does not exist in the namespace 'System'
I found some other code to set
AttachableMemberIdentifier and AttachablePropertyServices
then I got
The type or namespace name 'Activities' does not exist in the namespace 'System.ServiceModel'
​
E​ven though I'm adding the assemblyreferences for all 3 namespaces (my local solution, System.ServiceModel and System.Activities)​
The xamlx is the simple out of box one that it generates with GetData passing in an int and returns the int.ToString()
​What am I missing?
Code Following:
namespace WFStarterSolution
{
[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class RestService
{
static void CompileExpressions(DynamicActivity activity)
{
var activityName = activity.Name;
// Split activityName into Namespace and Type.Append _CompiledExpressionRoot to the type name
// to represent the new type that represents the compiled expressions.
// Take everything after the last . for the type name.
var activityType = activityName.Split('.').Last() + "_CompiledExpressionRoot";
// Take everything before the last . for the namespace.
var activityNamespace = string.Join(".", activityName.Split('.').Reverse().Skip(1).Reverse());
// Create a TextExpressionCompilerSettings.
var settings = new TextExpressionCompilerSettings
{
Activity = activity,
Language = "C#",
ActivityName = activityType,
ActivityNamespace = activityNamespace,
RootNamespace = null,//"CSharpExpression",
GenerateAsPartialClass = false,
AlwaysGenerateSource = true,
ForImplementation = true
};
// Compile the C# expression.
var results = new TextExpressionCompiler(settings).Compile();
// Any compilation errors are contained in the CompilerMessages.
if (results.HasErrors)
{
var cm = results.CompilerMessages.Aggregate(" ", (current, e) => current + (e.Number + " - "+e.Message + " : Line Number "+e.SourceLineNumber + ""));
throw new Exception("Compilation failed."+cm);
}
// Create an instance of the new compiled expression type.
var compiledExpressionRoot =
Activator.CreateInstance(results.ResultType,
new object[] { activity }) as ICompiledExpressionRoot;
// Attach it to the activity.
CompiledExpressionInvoker.SetCompiledExpressionRoot(
activity, compiledExpressionRoot);
}
[OperationContract]
public string DoWork()
{
// call WFService XAMLX somehow
var filepath = AppDomain.CurrentDomain.BaseDirectory;
try
{
var serviceImplementation = XamlServices.Load(filepath + "WFService.xamlx");
var service = serviceImplementation as WorkflowService;
if (service == null)
{
return "Failed";
}
else
{
var activity = service.Body;
var operand1 = new InArgument();
var dyanamicActivity = new DynamicActivity { Name = "WFServiceName", Implementation = () => activity};
var p = new DynamicActivityProperty
{
Name = "data",
Type = typeof(InArgument),
Value = operand1
};
dyanamicActivity.Properties.Add(p);
var impl = new AttachableMemberIdentifier(typeof(TextExpression), "NamespacesForImplementation");
var namespaces = new List { "WFStarterSolution" };
var ar = new[]
{
new AssemblyReference
{
Assembly = typeof (DynamicActivity).Assembly
},
new AssemblyReference
{
Assembly = typeof (RestService).Assembly
},
new AssemblyReference
{
Assembly = typeof (ServiceContractAttribute).Assembly
}
};
TextExpression.SetReferencesForImplementation(dyanamicActivity, ar);
AttachablePropertyServices.SetProperty(dyanamicActivity, impl, namespaces);
CompileExpressions(dyanamicActivity);
var iDict = new Dictionary() { { "data", 45} };
var output = WorkflowInvoker.Invoke(dyanamicActivity, iDict);
return "success";
}
}
catch (Exception ex)
{
return ex.Message+""+ex.StackTrace;
}
}
}
}
UPDATE *
If I add the following in to the AssemblyReference array
,new AssemblyReference
{
Assembly = typeof (WorkflowService).Assembly
}
, it compiles fine... but still gives the me original error of
Expression Activity type 'CSharpReference`1' requires compilation in order to run. Please ensure that the workflow has been compiled

Google AdWords v201206 DotNet Client GetClientReport trouble

Here's what I'm doing:
Selector selector = new Selector();
selector.fields = new string[] {"CampaignId", "AdGroupId", "Id", "CriteriaType", "Criteria", "CriteriaDestinationUrl", "Clicks", "Impressions", "Cost"};
Predicate predicate = new Predicate();
predicate.field = "Status";
predicate.#operator = PredicateOperator.IN;
predicate.values = new string[] { "ACTIVE", "PAUSED" };
selector.predicates = new Predicate[] { predicate };
ReportDefinition definition = new ReportDefinition();
definition.reportName = "criteria report";
definition.reportType = ReportDefinitionReportType.CRITERIA_PERFORMANCE_REPORT;
definition.downloadFormat = DownloadFormat.XML;
definition.dateRangeType = ReportDefinitionDateRangeType.YESTERDAY;
definition.selector = selector;
definition.includeZeroImpressions = true;
_handler.RunReport(new AdWordsUser(), definition);
And here's my handler method:
public void RunReport(AdWordsUser user, ReportDefinition definition)
{
if (definition != null)
{
string reportContents = string.Empty;
try
{
ReportUtilities utilities = new ReportUtilities(user);
utilities.ReportVersion = "v201206";
ClientReport report = utilities.GetClientReport(definition);
reportContents = report.Contents.ToString();
}
catch (Exception ex)
{
_errorHandler.AddError(ex);
}
}
}
Where I step through I get this error:
Report contents are invalid. - !!!2|||-1|||[ReportDefinitionError.CUSTOMER_SERVING_TYPE_REPORT_MISMATCH # selector]???
Been searching for hours trying to find a solution. Any hints?
It appears that the issue is indeed needing to pass in the customerClientID.
This document helped me picture what was going on.
I ended up modifying my code like this:
var configOptions = new Dictionary<string,string>();
configOptions.Add("clientCustomerID", customerID.ToString());
_handler.RunReport(new AdWordsUser(configOptions), definition);

Categories

Resources