Generating C# API client with NSwag.MSBuild - openapi

I have a .NET 6.0 API project. I am trying to generate a C# API client, so I added the NSwag.MSBuild nuget package and modified my API project file by adding the following:
<Target Name="NSwag" AfterTargets="PostBuildEvent" Condition=" '$(Configuration)' == 'Debug' ">
<Exec WorkingDirectory="$(ProjectDir)" EnvironmentVariables="ASPNETCORE_ENVIRONMENT=Development" Command="$(NSwagExe_Net60) run nswag.json /variables:Configuration=$(Configuration)" />
</Target>
I then created an nswag.json:
{
"runtime": "Net60",
"codeGenerators": {
"openApiToCSharpClient": {
"clientBaseClass": null,
"generateClientClasses": true,
"generateClientInterfaces": true,
"clientBaseInterface": null,
"injectHttpClient": true,
"disposeHttpClient": false,
"output": "../Blah/Client.g.cs",
"contractsOutputFilePath": "../Blah/Contracts.g.cs"
}
}
}
When the project builds it generates a client.g.cs in the Blah folder BUT the actual C# code it generates seems to be from some Pet API, for example here are a couple signatures in the IClient interface, I have no clue where this Pet API is coming from. Does anyone have any ideas:
/// <summary>
/// Add a new pet to the store
/// </summary>
/// <param name="accept_Language">The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US</param>
/// <param name="cookieParam">Some cookie</param>
/// <exception cref="ApiException">A server side error occurred.</exception>
System.Threading.Tasks.Task AddPetAsync(object body, string accept_Language, long cookieParam);
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <summary>
/// Add a new pet to the store
/// </summary>
/// <param name="accept_Language">The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US</param>
/// <param name="cookieParam">Some cookie</param>
/// <exception cref="ApiException">A server side error occurred.</exception>
System.Threading.Tasks.Task AddPetAsync(object body, string accept_Language, long cookieParam, System.Threading.CancellationToken cancellationToken);
/// <summary>
/// Update an existing pet
/// </summary>
/// <param name="accept_Language">The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US</param>
/// <param name="cookieParam">Some cookie</param>
/// <exception cref="ApiException">A server side error occurred.</exception>
System.Threading.Tasks.Task UpdatePetAsync(object body, string accept_Language, long cookieParam);
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <summary>
/// Update an existing pet
/// </summary>
/// <param name="accept_Language">The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US</param>
/// <param name="cookieParam">Some cookie</param>
/// <exception cref="ApiException">A server side error occurred.</exception>
System.Threading.Tasks.Task UpdatePetAsync(object body, string accept_Language, long cookieParam, System.Threading.CancellationToken cancellationToken);
/// <summary>
/// Find pet by ID
/// </summary>
/// <param name="petId">ID of pet to return</param>
/// <returns>successful operation</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
System.Threading.Tasks.Task<Pet> GetPetByIdAsync(long petId);

I think this is the default behavior of the NSwag generator. You have to specify input for reading your specification.
In my case i have create a yaml file for api specification.
nswag.json file looks like following.
{
"runtime": "Net60",
"defaultVariables": null,
"documentGenerator": {
"fromDocument": {
"json": "",
"url": "openapi.yaml",
"output": null
}
},
"codeGenerators": {
"openApiToCSharpClient": {
"clientBaseClass": null,
"generateClientClasses": true,
"generateClientInterfaces": true,
"clientBaseInterface": null,
"injectHttpClient": true,
"disposeHttpClient": false,
"output": "../Blah/Client.g.cs",
"contractsOutputFilePath": "../Blah/Contracts.g.cs"
}
}
}
For futher details you can read here.

Related

Create Log4net rolling file which is based on date

I need to create files for 45 separate locations (example: Boston, London, etc). And these file names have to be based on the date. Also can I provide a maximum file size to roll the files and the maximum number of files to roll.
Basically a file name must look like : Info_Boston_(2019.02.25).txt
So far I have come up with the below code to get by date. But I couldn't limit the file size to 1MB. The file grows beyond 1MB, and a new rolling file is not created. Please assist
<appender name="MyAppenderInfo" type="log4net.Appender.RollingFileAppender">
<param name="File" value="C:\\ProgramData\\Service\\Org\\Info"/>
<param name="RollingStyle" value="Date"/>
<param name="DatePattern" value="_(yyyy.MM.dd).\tx\t"/>
<param name="StaticLogFileName" value="false"/>
<maxSizeRollBackups value="10" />
<maximumFileSize value="1MB" />
<appendToFile value="true" />
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %message%n" />
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<levelMin value="DEBUG" />
<levelMax value="INFO" />
</filter>
</appender>
To address your specific post, I would not do this with a config based approach, as it would get rather cumbersome to manage I would think. A more programmatic approach would be to generate the logging instances dynamically.
EDIT: I took down the original to post this reworked example based on this SO post log4net: different logs on different file appenders at runtime
EDIT-2: I had to rework this again, as I realized I had omitted some required parts, and had some things wrong after the rework. This is tested and working. However, a few things to note, you will need to provide the using statements on the controller, to the logging class you make. next, you will need to DI your logging directories in as I have done, or come up with another method of providing the list of log file outputs.
This will allow you to very cleanly dynamically generate as many logging instances as you need to, to as many independent locations as you would like. I pulled this example from a project I did, and modified it a bit to fit your needs. Let me know if you have questions.
Create a Dynamic logger class which inherits from the base logger in the heirarchy:
using log4net;
using log4net.Repository.Hierarchy;
public sealed class DynamicLogger : Logger
{
private const string REPOSITORY_NAME = "somename";
internal DynamicLogger(string name) : base(name)
{
try
{
// try and find an existing repository
base.Hierarchy = (log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository(REPOSITORY_NAME);
} // try
catch
{
// it doesnt exist, make it.
base.Hierarchy = (log4net.Repository.Hierarchy.Hierarchy)LogManager.CreateRepository(REPOSITORY_NAME);
} // catch
} // ctor(string)
} // DynamicLogger
then, build out a class to manage the logging instances, and build the new loggers:
using log4net;
using log4net.Appender;
using log4net.Config;
using log4net.Core;
using log4net.Filter;
using log4net.Layout;
using log4net.Repository;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Linq;
public class LogFactory
{
private static List<ILog> _Loggers = new List<ILog>();
private static LoggingConfig _Settings;
private static ILoggerRepository _Repository;
public LogFactory(IOptions<LoggingConfig> configuration)
{
_Settings = configuration.Value;
ConfigureRepository(REPOSITORY_NAME);
} // ctor(IOptions<LoggingConfig>)
/// <summary>
/// Configures the primary logging repository.
/// </summary>
/// <param name="repositoryName">The name of the repository.</param>
private void ConfigureRepository(string repositoryName)
{
if(_Repository == null)
{
try
{
_Repository = LogManager.CreateRepository(repositoryName);
}
catch
{
// repository already exists.
_Repository = LogManager.GetRepository(repositoryName);
} // catch
} // if
} // ConfigureRepository(string)
/// <summary>
/// Gets a named logging instance, if it exists, and creates it if it doesnt.
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public ILog GetLogger(string name)
{
string filePath = string.Empty;
switch (name)
{
case "core":
filePath = _Settings.CoreLoggingDirectory;
break;
case "image":
filePath = _Settings.ImageProcessorLoggingDirectory;
break;
} // switch
if (_Loggers.SingleOrDefault(a => a.Logger.Name == name) == null)
{
BuildLogger(name, filePath);
} // if
return _Loggers.SingleOrDefault(a => a.Logger.Name == name);
} // GetLogger(string)
/// <summary>
/// Dynamically build a new logging instance.
/// </summary>
/// <param name="name">The name of the logger (Not file name)</param>
/// <param name="filePath">The file path you want to log to.</param>
/// <returns></returns>
private ILog BuildLogger(string name, string filePath)
{
// Create a new filter to include all logging levels, debug, info, error, etc.
var filter = new LevelMatchFilter();
filter.LevelToMatch = Level.All;
filter.ActivateOptions();
// Create a new pattern layout to determine the format of the log entry.
var pattern = new PatternLayout("%d %-5p %c %m%n");
pattern.ActivateOptions();
// Dynamic logger inherits from the hierarchy logger object, allowing us to create dynamically generated logging instances.
var logger = new DynamicLogger(name);
logger.Level = Level.All;
// Create a new rolling file appender
var rollingAppender = new RollingFileAppender();
// ensures it will not create a new file each time it is called.
rollingAppender.AppendToFile = true;
rollingAppender.Name = name;
rollingAppender.File = filePath;
rollingAppender.Layout = pattern;
rollingAppender.AddFilter(filter);
// allows us to dynamically generate the file name, ie C:\temp\log_{date}.log
rollingAppender.StaticLogFileName = false;
// ensures that the file extension is not lost in the renaming for the rolling file
rollingAppender.PreserveLogFileNameExtension = true;
rollingAppender.DatePattern = "yyyy-MM-dd";
rollingAppender.RollingStyle = RollingFileAppender.RollingMode.Date;
// must be called on all attached objects before the logger can use it.
rollingAppender.ActivateOptions();
logger.AddAppender(rollingAppender);
// Sets the logger to not inherit old appenders, or the core appender.
logger.Additivity = false;
// sets the loggers effective level, determining what level it will catch log requests for and log them appropriately.
logger.Level = Level.Info;
// ensures the new logger does not inherit the appenders of the previous loggers.
logger.Additivity = false;
// The very last thing that we need to do is tell the repository it is configured, so it can bind the values.
_Repository.Configured = true;
// bind the values.
BasicConfigurator.Configure(_Repository, rollingAppender);
LogImpl newLog = new LogImpl(logger);
_Loggers.Add(newLog);
return newLog;
} // BuildLogger(string, string)
} // LogFactory
Then, in your Dependency Injection you can inject your log factory. You can do that with something like this:
services.AddSingleton<LogFactory>();
Then in your controller, or any constructor really, you can just do something like this:
private LogFactory _LogFactory;
public HomeController(LogFactory logFactory){
_LogFactory = logFactory;
}
public async Task<IActionResult> Index()
{
ILog logger1 = _LogFactory.GetLogger("core");
ILog logger2 = _LogFactory.GetLogger("image");
logger1.Info("SomethingHappened on logger 1");
logger2.Info("SomethingHappened on logger 2");
return View();
}
This example will output:
2019-03-07 10:41:21,338 INFO core SomethingHappened on logger 1
in its own file called Core_2019-03-07.log
and also:
2019-03-07 11:06:29,155 INFO image SomethingHappened on logger 2
in its own file called Image_2019-03-07
Hope that makes more sense!

Microsoft CRM Plugin Infinite Loop

Another MS CRM question from me, I'm afraid. I've got the following code being executed on the update of a contact record but it gives me an error saying the job was cancelled because it includes an infinite loop. Can anyone tell me why this is happening, please?
// <copyright file="PostContactUpdate.cs" company="">
// Copyright (c) 2013 All Rights Reserved
// </copyright>
// <author></author>
// <date>8/7/2013 2:04:26 PM</date>
// <summary>Implements the PostContactUpdate Plugin.</summary>
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.1
// </auto-generated>
namespace Plugins3Test
{
using System;
using System.ServiceModel;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
/// <summary>
/// PostContactUpdate Plugin.
/// Fires when the following attributes are updated:
/// All Attributes
/// </summary>
public class PostContactUpdate: Plugin
{
/// <summary>
/// Initializes a new instance of the <see cref="PostContactUpdate"/> class.
/// </summary>
public PostContactUpdate()
: base(typeof(PostContactUpdate))
{
base.RegisteredEvents.Add(new Tuple<int, string, string, Action<LocalPluginContext>>(40, "Update", "contact", new Action<LocalPluginContext>(ExecutePostContactUpdate)));
// Note : you can register for more events here if this plugin is not specific to an individual entity and message combination.
// You may also need to update your RegisterFile.crmregister plug-in registration file to reflect any change.
}
/// <summary>
/// Executes the plug-in.
/// </summary>
/// <param name="localContext">The <see cref="LocalPluginContext"/> which contains the
/// <see cref="IPluginExecutionContext"/>,
/// <see cref="IOrganizationService"/>
/// and <see cref="ITracingService"/>
/// </param>
/// <remarks>
/// For improved performance, Microsoft Dynamics CRM caches plug-in instances.
/// The plug-in's Execute method should be written to be stateless as the constructor
/// is not called for every invocation of the plug-in. Also, multiple system threads
/// could execute the plug-in at the same time. All per invocation state information
/// is stored in the context. This means that you should not use global variables in plug-ins.
/// </remarks>
protected void ExecutePostContactUpdate(LocalPluginContext localContext)
{
if (localContext == null)
{
throw new ArgumentNullException("localContext");
}
// TODO: Implement your custom Plug-in business logic.
// Obtain the execution context from the service provider.
IPluginExecutionContext context = localContext.PluginExecutionContext;
IOrganizationService service = localContext.OrganizationService;
IServiceProvider serviceProvider = localContext.ServiceProvider;
ITracingService tracingService = localContext.TracingService;
// Obtain the target entity from the input parmameters.
//Entity contextEntity = (Entity)context.InputParameters["Target"];
Entity targetEntity = null;
targetEntity = (Entity)context.InputParameters["Target"];
Guid cid = targetEntity.Id;
ColumnSet cols = new ColumnSet("jobtitle");
Entity contact = service.Retrieve("contact", cid, cols);
contact.Attributes["jobtitle"] = "Sometitle";
service.Update(contact);
}
}
}
it's happening because your plugin is executed when a contact is updated and the last line of your code update the contact again, this cause to call again the plugin ...
Then you have your infinite loop
You can prevent the loop using the IExecutionContext.Depth property
http://msdn.microsoft.com/en-us/library/microsoft.xrm.sdk.iexecutioncontext.depth.aspx
However if you explain your requirement I think it's possible to find a solution.
At first if IExecutionContext.Depth <= 1 seems like a great idea, but it can bite you if you have a different plugin that updates the contact. You should be using the SharedVariables of the plugin context.
Something like this should work:
Add this declaration to the plugin class as a class level field:
public static readonly Guid HasRunKey = new Guid("{6339dc20-01ce-4f2f-b4a1-0a1285b65bff}");
And add this as the first step of your plugin:
if(context.SharedVariables.ContainsKey[HasRunKey]){
return;
}else{
context.SharedVariables.Add(HasRunKey);
// Proceed with plugin execution
}
**I went through a lot of trial and error. I don't know why plugin context does not work but this works but the parentcontext works. This (workaround?) works :)
**
if (this.Context.ParentContext != null && this.Context.ParentContext.ParentContext != null)
{
var assemblyName = Assembly.GetExecutingAssembly().GetName().Name;
if (!this.Context.ParentContext.ParentContext.SharedVariables.Contains(assemblyName))
{
this.Context.ParentContext.ParentContext.SharedVariables.Add(assemblyName, true.ToString() );
}
else
{
// isRecursive = true;
return;
}
}
Your plugin is updating the "jobtitle" field, I'm not sure if this plugin is being triggered by all contact updates, or you have set some FilteringAttributes to it in the Registerfile.crmregister Plugin's definition. By excluding the "jobtitle" field from the attributes that trigger this plugin you can solve your issue.

Custom SpecFlow TestGeneratorProvider registration causes NullReferenceException at test generation

I'm trying to add my CodedUI test generator plugin in SpecFlow 1.9. I'm trying to use the new plugin registration, since the old type of registration causes all kinds of issues in our solution, since we prefer not to put the custom Generator Provider in the Specflow installation directory (which is different on each developers machine).
I started with adding the new Specflow.CustomPlugin NuGet package to a .NET 4.0 class library.
Based on the samples for the CodedUI Generator, I pieced this together:
using TechTalk.SpecFlow.Infrastructure;
[assembly: GeneratorPlugin(typeof(CodedUIGeneratorProvider.Generator.SpecFlowPlugin.CodedUIGeneratorPlugin))]
namespace CodedUIGeneratorProvider.Generator.SpecFlowPlugin
{
using System.CodeDom;
using BoDi;
using TechTalk.SpecFlow.Generator;
using TechTalk.SpecFlow.Generator.Configuration;
using TechTalk.SpecFlow.Generator.Plugins;
using TechTalk.SpecFlow.Generator.UnitTestProvider;
using TechTalk.SpecFlow.UnitTestProvider;
using TechTalk.SpecFlow.Utils;
/// <summary>
/// The CodedUI generator plugin.
/// </summary>
public class CodedUIGeneratorPlugin : IGeneratorPlugin
{
/// <summary>
/// The register dependencies.
/// </summary>
/// <param name="container">
/// The container.
/// </param>
public void RegisterDependencies(ObjectContainer container)
{
}
/// <summary>
/// The register customizations.
/// </summary>
/// <param name="container">
/// The container.
/// </param>
/// <param name="generatorConfiguration">
/// The generator configuration.
/// </param>
public void RegisterCustomizations(ObjectContainer container, SpecFlowProjectConfiguration generatorConfiguration)
{
container.RegisterTypeAs<CodedUIGeneratorProvider, IUnitTestGeneratorProvider>("default");
container.RegisterTypeAs<MsTest2010RuntimeProvider, IUnitTestRuntimeProvider>("default");
}
/// <summary>
/// The register configuration defaults.
/// </summary>
/// <param name="specFlowConfiguration">
/// The spec flow configuration.
/// </param>
public void RegisterConfigurationDefaults(SpecFlowProjectConfiguration specFlowConfiguration)
{
}
}
/// <summary>
/// The CodedUI generator.
/// </summary>
public class CodedUIGeneratorProvider : MsTest2010GeneratorProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="CodedUiGeneratorProvider"/> class.
/// </summary>
/// <param name="codeDomHelper">
/// The code dom helper.
/// </param>
public CodedUIGeneratorProvider(CodeDomHelper codeDomHelper)
: base(codeDomHelper)
{
}
/// <summary>
/// The set test class.
/// </summary>
/// <param name="generationContext">
/// The generation context.
/// </param>
/// <param name="featureTitle">
/// The feature title.
/// </param>
/// <param name="featureDescription">
/// The feature description.
/// </param>
public override void SetTestClass(TestClassGenerationContext generationContext, string featureTitle, string featureDescription)
{
base.SetTestClass(generationContext, featureTitle, featureDescription);
foreach (CodeAttributeDeclaration declaration in generationContext.TestClass.CustomAttributes)
{
if (declaration.Name == "Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute")
{
generationContext.TestClass.CustomAttributes.Remove(declaration);
break;
}
}
generationContext.TestClass.CustomAttributes.Add(new CodeAttributeDeclaration(new CodeTypeReference("Microsoft.VisualStudio.TestTools.UITesting.CodedUITestAttribute")));
}
}
}
And when trying to configure it as follows:
<specFlow>
<!-- For additional details on SpecFlow configuration options see http://go.specflow.org/doc-config -->
<plugins>
<add name="CodedUIGeneratorProvider" path="." type="GeneratorAndRuntime"/>
</plugins>
<generator dependencies="CodedUIGeneratorProvider"/>
<runtime dependencies="CodedUIGeneratorProvider"/>
</specFlow>
I get the following error message:
error Generation error: SpecFlow configuration error -> The value of the property 'dependencies' cannot be parsed. The error is: Object reference not set to an instance of an object.
The error is caused inside the SpecFlow configuration code it seems, the stack trace is as follows:
System.Configuration.dll!System.Configuration.ConfigurationProperty.ConvertFromString(string value) + 0x2a bytes
System.Configuration.dll!System.Configuration.ConfigurationElement.DeserializePropertyValue(System.Configuration.ConfigurationProperty prop, System.Xml.XmlReader reader) + 0x36 bytes
System.Configuration.dll!System.Configuration.ConfigurationElement.DeserializeElement(System.Xml.XmlReader reader, bool serializeCollectionKey) + 0x221 bytes
System.Configuration.dll!System.Configuration.ConfigurationElement.DeserializeElement(System.Xml.XmlReader reader, bool serializeCollectionKey) + 0x78e bytes
System.Configuration.dll!System.Configuration.ConfigurationSection.DeserializeSection(System.Xml.XmlReader reader) + 0x3c bytes
TechTalk.SpecFlow.dll!TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler.CreateFromXml(string xmlContent) + 0xb0 bytes
TechTalk.SpecFlow.Generator.dll!TechTalk.SpecFlow.Generator.Configuration.GeneratorConfigurationProvider.LoadConfiguration(TechTalk.SpecFlow.Generator.Interfaces.SpecFlowConfigurationHolder configurationHolder, TechTalk.SpecFlow.Generator.Configuration.SpecFlowProjectConfiguration configuration) + 0x41 bytes
TechTalk.SpecFlow.Vs2010Integration.dll!TechTalk.SpecFlow.Vs2010Integration.Generator.VsGeneratorInfoProvider.GenGeneratorConfig() + 0x52 bytes
TechTalk.SpecFlow.Vs2010Integration.dll!TechTalk.SpecFlow.Vs2010Integration.Generator.VsGeneratorInfoProvider.GetGeneratorInfo() + 0x3a bytes
TechTalk.SpecFlow.IdeIntegration.dll!TechTalk.SpecFlow.IdeIntegration.Generator.RemoteGeneratorServices.GetGeneratorInfo() Line 38 + 0x9 bytesC#
TechTalk.SpecFlow.IdeIntegration.dll!TechTalk.SpecFlow.IdeIntegration.Generator.RemoteGeneratorServices.GetTestGeneratorFactoryForCreate() Line 43 + 0xa bytesC#
TechTalk.SpecFlow.IdeIntegration.dll!TechTalk.SpecFlow.IdeIntegration.Generator.GeneratorServices.CreateTestGenerator() Line 24 + 0xa bytesC#
TechTalk.SpecFlow.IdeIntegration.dll!TechTalk.SpecFlow.IdeIntegration.Generator.IdeSingleFileGenerator.GenerateCode(string inputFilePath, string inputFileContent, TechTalk.SpecFlow.IdeIntegration.Generator.GeneratorServices generatorServices, TechTalk.SpecFlow.Generator.Interfaces.ProjectSettings projectSettings) Line 38 + 0x29 bytesC#
TechTalk.SpecFlow.IdeIntegration.dll!TechTalk.SpecFlow.IdeIntegration.Generator.IdeSingleFileGenerator.Generate(string inputFilePath, string inputFileContent, TechTalk.SpecFlow.IdeIntegration.Generator.GeneratorServices generatorServices, TechTalk.SpecFlow.Utils.CodeDomHelper codeDomHelper, TechTalk.SpecFlow.Generator.Interfaces.ProjectSettings projectSettings) Line 23 + 0x12 bytesC#
TechTalk.SpecFlow.IdeIntegration.dll!TechTalk.SpecFlow.IdeIntegration.Generator.IdeSingleFileGenerator.GenerateFile(string inputFilePath, string outputFilePath, System.Func<TechTalk.SpecFlow.IdeIntegration.Generator.GeneratorServices> generatorServicesProvider, System.Func<string,string> inputFileContentProvider, System.Action<string,string> outputFileContentWriter) Line 92 + 0x13 bytesC#
TechTalk.SpecFlow.Vs2010Integration.dll!TechTalk.SpecFlow.VsIntegration.SingleFileGenerator.SpecFlowSingleFileGeneratorBase.GenerateInternal(string inputFilePath, string inputFileContent, EnvDTE.Project project, string defaultNamespace, System.Action<TechTalk.SpecFlow.VsIntegration.SingleFileGenerator.SingleFileGeneratorError> onError, out string generatedContent) + 0x18e bytes
TechTalk.SpecFlow.Vs2010Integration.dll!TechTalk.SpecFlow.VsIntegration.SingleFileGenerator.SingleFileGeneratorBase.Generate(string inputFilePath, string inputFileContents, string defaultNamespace, System.IntPtr[] rgbOutputFileContents, out uint pcbOutput, Microsoft.VisualStudio.Shell.Interop.IVsGeneratorProgress generateProgress) + 0xc3 bytes
[Native to Managed Transition]
Which reflector tells me can only mean that TechTalk.SpecFlow.IdeIntegration.Generator.RemoteGeneratorServices.generatorInfoProvider is null.
I'm at a loss on how to fix this. There's very few documentation available to resolve this.
The Question:
If I can get this to work I'd be very happy. altenatively I'd love to see a way to configure the 'old way' without having to put files in the SpecFlow installation directory.
I found the issue :)
In the Register Customizations method of the plugin, don't register using a name, just use the RegisterTypeAs method:
public void RegisterCustomizations(ObjectContainer container, SpecFlowProjectConfiguration generatorConfiguration)
{
container.RegisterTypeAs<CodedUIGeneratorProvider, IUnitTestGeneratorProvider>();
}
Then the config looks very simple, like this:
<specFlow>
<!-- For additional details on SpecFlow configuration options see http://go.specflow.org/doc-config -->
<plugins>
<add name="CodedUIGeneratorProvider" path="." type="Generator"/>
</plugins>
</specFlow>
This way you can drop the plugin assembly (which must be named *.SpecflowPlugin.dll) in the project directory, or use a relative path from the project directory and set it in the path=".\Lib" property of the plugins\add item.
For more information see: https://jessehouwing.net/specflow-custom-unit-test-generator/
For developers coming here with SpecFlow 2.1.0, please add the following to the CodedUIGeneratorProvider class above:
public void Initialize(GeneratorPluginEvents generatorPluginEvents, GeneratorPluginParameters generatorPluginParameters)
{
generatorPluginEvents.CustomizeDependencies += GeneratorPluginEvents_CustomizeDependencies;
}
private void GeneratorPluginEvents_CustomizeDependencies(object sender, CustomizeDependenciesEventArgs eventArgs)
{
eventArgs.ObjectContainer.RegisterTypeAs<CodedUIGeneratorProvider, IUnitTestGeneratorProvider>();
}
The interface of the CustomPlugin interface has changed, making it necessary to hook up to customization events in order to get the customization registered.
See https://github.com/techtalk/SpecFlow/wiki/Plugins

Converting values (kg to lbs) in MVVM pattern

I am building an app that can track weight. I allow the user to select their unit preference of kg or lbs. I want to keep the data in the DB standard (kg) so if the user selects lbs as their preference I need the data converted from kg to lbs in the UI.
This conversion can be handled very easily in the ViewModel.
This is one of the huge advantages of having a ViewModel - you can have easily testable logic, such as conversion between units, which stays completely separate from the user interface code.
I suggest you to use it Coverter Property of DataBinding and resolve it by XAML.
DataBinding = { Binding Path = YourProperty,Converter = {StaticResource YourConverter}}
YourConverter must implement the IValueConverter inferface (msdn doc) and has to be declare as Resource.
I would prefer this approach because keep the conversion logic on the View Side.
I'm supposing we are in WPF. Use a Binding Converter and create an object implementing IValueConverter doing the proper configured conversion.
The best way I can explain to do this is to use a custom converter interface. Instead of talking on I'll just zap you an example.
private IConverter<Double, Double> weightConverter = new KgToLbsConverter();
private double _weight;
public double Weight
{
get
{
return weightConverter.ConvertFrom(_weight);
}
set
{
_weight = weightConverter.ConvertTo(value);
RaisePropertyChanged(() => Weight);
}
}
/// <summary>
/// Provides a simple converter contract.
/// </summary>
/// <typeparam name="T">The source Type</typeparam>
/// <typeparam name="R">The target Type.</typeparam>
public interface IConverter<T, R>
{
R ConvertFrom(T value);
T ConvertTo(R value);
}
/// <summary>
/// Provides a converter to change Kg to Lbs and vice versa
/// </summary>
public class KgToLbsConverter : IConverter<Double, Double>
{
/// <summary>
/// From Kg to Lbs.
/// </summary>
/// <param name="value">The weight in Kg.</param>
/// <returns>The weight in Lbs.</returns>
public double ConvertFrom(double value)
{
return value / 2.2;
}
/// <summary>
/// From Lbs to Kg.
/// </summary>
/// <param name="value">The weight in Lbs.</param>
/// <returns>The weight in Kg.</returns>
public double ConvertTo(double value)
{
return value * 2.2;
}
}
Using this you could make can any converters you want, and then let the user select from concrete implementations.
Note: The reason I prefer this over converters is view models may need to know what conversion mode is active and how to work with them, this adds quite a bit of flexibility. When you need a single conversion for a V-VM relationship then IValueConverter is the way to go, but for your scenario, this is easier to maintain and to extend.

JDEdwards XMLInterop

Wondering if anybody out there has any success in using the JDEdwards XMLInterop functionality. I've been using it for a while (with a simple PInvoke, will post code later). I'm looking to see if there's a better and/or more robust way.
Thanks.
As promised, here is the code for integrating with JDEdewards using XML. It's a webservice, but could be used as you see fit.
namespace YourNameSpace
{
/// <summary>
/// This webservice allows you to submit JDE XML CallObject requests via a c# webservice
/// </summary>
[WebService(Namespace = "http://WebSite.com/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class JdeBFService : System.Web.Services.WebService
{
private string _strServerName;
private UInt16 _intServerPort;
private Int16 _intServerTimeout;
public JdeBFService()
{
// Load JDE ServerName, Port, & Connection Timeout from the Web.config file.
_strServerName = ConfigurationManager.AppSettings["JdeServerName"];
_intServerPort = Convert.ToUInt16(ConfigurationManager.AppSettings["JdePort"], CultureInfo.InvariantCulture);
_intServerTimeout = Convert.ToInt16(ConfigurationManager.AppSettings["JdeTimeout"], CultureInfo.InvariantCulture);
}
/// <summary>
/// This webmethod allows you to submit an XML formatted jdeRequest document
/// that will call any Master Business Function referenced in the XML document
/// and return a response.
/// </summary>
/// <param name="Xml"> The jdeRequest XML document </param>
[WebMethod]
public XmlDocument JdeXmlRequest(XmlDocument xmlInput)
{
try
{
string outputXml = string.Empty;
outputXml = NativeMethods.JdeXmlRequest(xmlInput, _strServerName, _intServerPort, _intServerTimeout);
XmlDocument outputXmlDoc = new XmlDocument();
outputXmlDoc.LoadXml(outputXml);
return outputXmlDoc;
}
catch (Exception ex)
{
ErrorReporting.SendEmail(ex);
throw;
}
}
}
/// <summary>
/// This interop class uses pinvoke to call the JDE C++ dll. It only has one static function.
/// </summary>
/// <remarks>
/// This class calls the xmlinterop.dll which can be found in the B9/system/bin32 directory.
/// Copy the dll to the webservice project's /bin directory before running the project.
/// </remarks>
internal static class NativeMethods
{
[DllImport("xmlinterop.dll",
EntryPoint = "_jdeXMLRequest#20",
CharSet = CharSet.Auto,
ExactSpelling = false,
CallingConvention = CallingConvention.StdCall,
SetLastError = true)]
private static extern IntPtr jdeXMLRequest([MarshalAs(UnmanagedType.LPWStr)] StringBuilder server, UInt16 port, Int32 timeout, [MarshalAs(UnmanagedType.LPStr)] StringBuilder buf, Int32 length);
public static string JdeXmlRequest(XmlDocument xmlInput, string strServerName, UInt16 intPort, Int32 intTimeout)
{
StringBuilder sbServerName = new StringBuilder(strServerName);
StringBuilder sbXML = new StringBuilder();
XmlWriter xWriter = XmlWriter.Create(sbXML);
xmlInput.WriteTo(xWriter);
xWriter.Close();
string result = Marshal.PtrToStringAnsi(jdeXMLRequest(sbServerName, intPort, intTimeout, sbXML, sbXML.Length));
return result;
}
}
}
You have to send it messages like the following one:
<jdeRequest type='callmethod' user='USER' pwd='PWD' environment='ENV'>
<callMethod name='GetEffectiveAddress' app='JdeWebRequest' runOnError='no'>
<params>
<param name='mnAddressNumber'>10000</param>
</params>
</callMethod>
</jdeRequest>
To anyone trying to do this, there are some dependencies to xmlinterop.dll.
you'll find these files on the fat client here ->c:\E910\system\bin32
this will create a 'thin client'
PSThread.dll
icudt32.dll
icui18n.dll
icuuc.dll
jdel.dll
jdeunicode.dll
libeay32.dll
msvcp71.dll
ssleay32.dll
ustdio.dll
xmlinterop.dll
I changed our JDE web service to use XML Interop after seeing this code, and we haven't had any stability problems since. Previously we were using the COM Connector, which exhibited regular communication failures (possibly a connection pooling issue?) and was a pain to install and configure correctly.
We did have issues when we attempted to use transactions, but if you're doing simple single business function calls this shouldn't be an problem.
Update: To elaborate on the transaction issues - if you're attempting to keep a transaction alive over multiple calls, AND the JDE application server is handling a modest number of concurrent calls, the xmlinterop calls start returning an 'XML response failed' message and the DB transaction is left open with no way to commit or rollback. It's possible tweaking the number of kernels might solve this, but personally, I'd always try to complete the transaction in a single call.