I have an app that has two components. One is a C# console app that will run as a service in production. The other component is the UI which is a WPF app that runs as a system tray app. This article is what I used to get off the ground. Both apps target .NET6.
I need to send messages through the named pipe, which is implemented with the H.pipes nuget package, from both the console app and the WPF app. I am currently defining the named pipes object as a static variable in a static class so I can access it across multiple classes. Like so:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WindowsTapAgent
{
internal static class Globals
{
public static NamedPipesServer pipeServer;
}
}
The named pipe is initialized in the console app like this:
using Newtonsoft.Json;
using Serilog;
using WindowsTapAgent;
using System.Net.NetworkInformation;
using System.Reflection;
string logFileName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\" + "DrawbridgeAgent.log";
Log.Logger = new LoggerConfiguration()
// add a rolling file for all logs
.WriteTo.File(logFileName,
fileSizeLimitBytes: 2000000)
.WriteTo.Console()
.WriteTo.EventLog("Drawbridge Agent Source", manageEventSource: true)
// set default minimum level
.MinimumLevel.Information()
.CreateLogger();
Globals.pipeServer = new NamedPipesServer();
Globals.pipeServer.InitializeAsync().GetAwaiter().GetResult();
...
...
...
...
The actual code that accesses the named pipe is as follows:
using Serilog;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using System.Net.Http.Headers;
using System.Text;
namespace WindowsTapAgent
{
public class TapDevice
{
public RsaKey RsaKey { get; set; }
//private NamedPipesServer PipeServer { get; set; }
public TapDevice(AgentConfigInfo info)
{
Log.Information("In TapDevice Constructor");
string containerName = info.RsaKeyContainerName;
Log.Information("ContainerName: " + containerName);
RsaKey = new RsaKey(containerName);
Log.Information("RSAKey Public Key: " + RsaKey.getPublicKeyData());
}
public async Task<AgentConfigInfo> Register(RegistrationToken regToken, AgentConfigInfo info)
{
try
{
Log.Information("-----------------------------Attempting to register------------------------------.");
if (Globals.pipeServer != null)
Globals.pipeServer.SendMessage("Registering with Drawbridge");
...
...
...
NamedPipeServer class:
using H.Pipes;
using H.Pipes.Args;
using Common;
namespace WindowsTapAgent
{
public class NamedPipesServer : IDisposable
{
const string PIPE_NAME = "drawbridgepipe";
private PipeServer<PipeMessage> server;
private PipeConnection<PipeMessage> connection;
public async Task InitializeAsync()
{
server = new PipeServer<PipeMessage>(PIPE_NAME);
server.ClientConnected += async (o, args) => await OnClientConnectedAsync(args);
server.ClientDisconnected += (o, args) => OnClientDisconnected(args);
server.MessageReceived += (sender, args) => OnMessageReceived(args.Message);
server.ExceptionOccurred += (o, args) => OnExceptionOccurred(args.Exception);
await server.StartAsync();
}
private async Task OnClientConnectedAsync(ConnectionEventArgs<PipeMessage> args)
{
//Console.WriteLine($"Client {args.Connection.Id} is now connected!");
connection = args.Connection;
}
...
...
...
The problem is that the TapDevice class is used in both the console app and the WPF app. When the PipeServer object is accessed through the TapDevice class from the console app (where it's also declared and initialized) everything works fine. When it's called from the WPF app, again through the TapDevice class, the PipeServer object is always null, and can't be used. How should I better structure this project?
I figured it out. I had the client and server mixed up. I needed to put the server on the UI side, then I can can create multiple client connections to it as needed. This eliminates the need for the messy global variable that didn't work anyways.
Related
How can I force a Dotnet6 Desktop Winforms App to use appsettings.json instead of Settings.settings[App.config]?
If I create a new desktop application in Visual Studio 2022 to use Dotnet6.0, it, by default will create a Settings.settings file on the fly, then create an App.config file as well (which is odd as this was supposedly deprecated in Dotnet5.0 and up!).
Unfortunately, all settings are embedded in the application after installation/publishing and cannot be changed anywhere in the application folder, once installed.
For older DotNet4.72 desktop applications, the app.config was always in the release output and, if changed, the app would read any changes just fine upon execution.
I'd like to use the appsettings.json just like for web apps (dotnet6.0) and there's a lot of info on how to do this with console apps (dotnet6.0), but not for desktop windows.
I have quite a few DotNet4.72 Desktop Apps that I wish to convert that still require the ability to simply change a configuration setting by editing a file on the fly (if conditions change). While I can roll my own solution, I'd like to do this similarly like in my web applications (appsettings.json). Better yet, I'd love to see a VS2022 desktop template that does this every time a new desktop app is created for DotNet6 and 7.
Any ideas?
Basically you can use the Microsoft.Extensions.Configuration but you can also make use of Microsoft.Extensions.Hosting nuget package as well.
Here is a tested working example that handles configuration change as well.
Steps:
Add an appsettings.json file to the project.
Set Build Action to None.
Set Copy to Output Directory to Copy always.
Create a class for your options. (In this example: SampleOptions)
Configure your options in appsettings.json.
Add Microsoft.Extensions.Hosting nuget package to your project.
Modify Program.cs:
Configure and build a host. (e.g. register your forms, options into DI)
Start host.
Resolve IHostApplicationLifetime.
Create an IServiceScope using host's IServiceProvider.
Resolve your MainForm.
Call Application.Run() method with your resolved MainForm instance.
Modify MainForm.cs:
Inject IOptionsMonitor<SampleOptions> into the .ctor
Register a listener to be called whenever SampleOptions changes
using IOptionsMonitor<SampleOptions>.OnChange method.
Here are the files.
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"SampleOptions": {
"SampleStringKey": "Sample value",
"SampleIntegerKey": 42
}
}
SampleOptions.cs
namespace WinFormsAppNet6;
public class SampleOptions
{
public string SampleStringKey { get; set; } = "N/A";
public int SampleIntegerKey { get; set; }
}
Program.cs
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace WinFormsAppNet6;
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static async Task Main() // <-- 'async Task' instead of 'void'
{
// To customize application configuration such as
// set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
using IHost host = CreateHost();
await host.StartAsync();
IHostApplicationLifetime lifetime =
host.Services.GetRequiredService<IHostApplicationLifetime>();
using (IServiceScope scope = host.Services.CreateScope())
{
var mainForm = scope.ServiceProvider.GetRequiredService<MainForm>();
Application.Run(mainForm);
}
lifetime.StopApplication();
await host.WaitForShutdownAsync();
}
private static IHost CreateHost()
{
string[] args = Environment.GetCommandLineArgs().Skip(1).ToArray();
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<MainForm>();
builder.Services.Configure<SampleOptions>(
builder.Configuration.GetSection(nameof(SampleOptions))
);
return builder.Build();
}
}
MainForm.cs
Controls:
Button: UpdateConfigurationButton
Button: ExitButton
RichTextBox: AppLog
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Windows.Forms;
using Microsoft.Extensions.Options;
namespace WinFormsAppNet6;
public partial class MainForm : Form
{
private readonly IOptionsMonitor<SampleOptions> optionsMonitor;
private readonly JsonSerializerOptions jsonSerializerOptions =
new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
public MainForm(IOptionsMonitor<SampleOptions> optionsMonitor)
{
InitializeComponent();
this.optionsMonitor = optionsMonitor;
optionsMonitor.OnChange(OnOptionsChange);
LogOptions(optionsMonitor.CurrentValue);
}
private void OnOptionsChange(SampleOptions options)
{
LogOptions(options);
}
private void LogOptions(
SampleOptions options,
[CallerMemberName] string? callerMemberName = null
)
{
AppendLog(
Environment.NewLine + JsonSerializer.Serialize(options),
callerMemberName
);
}
private void AppendLog(
string message,
[CallerMemberName] string? callerMemberName = null
)
{
if (AppLog.InvokeRequired)
{
AppLog.Invoke(() => AppendLog(message, callerMemberName));
return;
}
AppLog.AppendText(
$"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.ffffff} [{nameof(MainForm)}]::[{callerMemberName ?? "Unknown"}] {message}{Environment.NewLine}"
);
}
private void UpdateConfigurationButton_Click(object sender, EventArgs e)
{
const string AppSettingsJsonFileName = "appsettings.json";
if (!File.Exists(AppSettingsJsonFileName))
{
AppendLog(
$"{nameof(FileNotFoundException)}: {AppSettingsJsonFileName}"
);
return;
}
string jsonContent = File.ReadAllText(AppSettingsJsonFileName);
JsonNode? rootNode = JsonNode.Parse(jsonContent);
if (rootNode is null)
{
AppendLog($"{nameof(JsonException)}: File parse failed.");
return;
}
AppendLog(
$"Finding key: {nameof(SampleOptions)}:{nameof(SampleOptions.SampleStringKey)}"
);
JsonObject rootObject = rootNode.AsObject();
JsonObject? optionsObject = rootObject[nameof(SampleOptions)]?.AsObject();
if (optionsObject is null)
{
AppendLog(
$"{nameof(KeyNotFoundException)}: {nameof(SampleOptions)}:{nameof(SampleOptions.SampleStringKey)}"
);
return;
}
AppendLog(
$"Updating key: {nameof(SampleOptions)}:{nameof(SampleOptions.SampleStringKey)}"
);
optionsObject[nameof(SampleOptions.SampleStringKey)] =
JsonValue.Create(
$"Value modified at {DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.ffffff}"
);
AppendLog($"Saving file: {AppSettingsJsonFileName}");
using var stream =
new FileStream(
AppSettingsJsonFileName,
FileMode.OpenOrCreate,
FileAccess.Write,
FileShare.None
);
jsonContent = rootObject.ToJsonString(jsonSerializerOptions);
stream.Write(Encoding.UTF8.GetBytes(jsonContent));
stream.Flush();
}
private void ExitButton_Click(object? sender, EventArgs e)
{
Application.Exit();
}
}
Screenshot
I have a weird error in Redis on .Net 6. When I run the test code here:
https://github.com/redis-developer/redis-graph-dotnet-basic-app/blob/main/Program.cs
It works perfectly fine. In this case the code is running in the program.cs file.
When I port that code to a class, in order to better manage encapsulation and complexity. It does not work. What it does is run the code and when it gets to the: await graph.QueryAsync part, it just stops the debugger. Very strange indeed.
Here is the code I am using. Any thoughts or suggestions:
//Program.cs (Relevant Bits)
using RedisTest //PROGRAM //WRITE TO REDIS ENTERPRISE CLOUD Process_LoadGraph process_LoadGraph = new Process_LoadGraph(); process_LoadGraph.Controller(results);
//SHARED CONNECTION CLASS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using StackExchange.Redis;
namespace RedisTest
{
public class RedisSharedConnection
{
public static ConnectionMultiplexer Connection
{
get
{
return lazyConnection.Value;
}
}
private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() =>
{
ConnectionMultiplexer connectionMultiplexer = ConnectionMultiplexer.Connect(ConfigData.dbConnectionString);
return connectionMultiplexer;
});
}
}
//USAGE CLASS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NRedisGraph;
namespace RedisTest
{
public class Process_LoadGraph
{
public async void Controller(List<Result> results)
{
//Setup
var redisConnection = RedisSharedConnection.Connection;
//var redisConnection = ConnectionMultiplexer.Connect(ConfigData.dbConnectionString);
var db = redisConnection.GetDatabase(ConfigData.dbId);
var graph = new RedisGraph(db);
string graphName = ConfigData.graphName;
//Test Transaction
// Create Bob
// CRASHES HERE
var createBobResult = await graph.QueryAsync("pets", "MERGE(:human{name:'Bob',age:32})");
}
}
}
Turns out the solution is to use Redis in a static class. Along the following lines:
internal static class WriteToDB
{
public static async Task WriteAsync(List<string> querieS)
{
//Load Graph
//Setup
var redisConnection = RedisSharedConnection.Connection;
//var redisConnection = ConnectionMultiplexer.Connect(ConfigData.dbConnectionString);
var db = redisConnection.GetDatabase(ConfigData.dbId);
var graph = new RedisGraph(db);
string graphName = ConfigData.graphName;
// ** DEBUG
//Test Transaction
// Create Bob
var createBobResult = await graph.QueryAsync("pets", "MERGE(:human{name:'Bob',age:32})");
{ }
//Clear Graph
await graph.QueryAsync(graphName, "MATCH(n) DETACH DELETE n");
{ }
}
}
I am trying to rework my Azure API client to use singleton HttpClient for ServiceClient<T>, since multiple sources suggested to do that (for performance, and it is also suggested approach for HttpClient which is used behind the scenes for Microsoft.Rest.ServiceClient "in general")
I am using Microsoft.Rest.ClientRuntime.2.3.12 version
I see that Microsoft.Rest.ServiceClient has constructor for reusing HttpClient
protected ServiceClient(HttpClient httpClient, bool disposeHttpClient = true);
This is constructor for my API client in order to reuse HttpClient, but when it is called it fails with System.MissingMethodException: Void Microsoft.Rest.ServiceClient`1..ctor(System.Net.Http.HttpClient, Boolean)
public MyAPIclient(ServiceClientCredentials credentials, HttpClient client)
: base(client, disposeHttpClient: false)
{
this._baseUri = new Uri("https://...");
if (credentials == null)
{
throw new ArgumentNullException("credentials");
}
this.Credentials = credentials;
Credentials?.InitializeServiceClient(this);
}
This is how I call API client
using (var apiClient = new MyAPIclient(credentials, HttpClientSingleton.Client))
//I get Method not found 'Void Microsoft.Rest.ServiceClient`1..ctor(System.Net.Http.HttpClient, Boolean)'
{
//some api call
}
This is how I instantiate my http client singleton
public static class HttpClientSingleton
{
public static readonly HttpClient Client = new HttpClient() { Timeout = TimeSpan.FromMinutes(30) };
}
Why do I get this exception (I can see that ServiceClient has ctor(httpClient, bool))?
How to fix this problem?
Like Nkosi said, you should try restoring the nuget packages, clearing your obj/bin and rebuilding. This usually happens when things get out of step. With all the shadow caching etc etc things can end up mismatched. I created a small example based on your code and there weren't any issues with it. If you find that still doesn't work you should include more info on your file, like the class declarations and your using statements, and what version of .net you are using.
The below works for me for 4.5.2 ServiceClient 2.3.12
using Microsoft.Rest;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
static void Main(string[] args)
{
using (var apiClient = new MyAPIclient(null, HttpClientSingleton.Client))
//I get Method not found 'Void Microsoft.Rest.ServiceClient`1..ctor(System.Net.Http.HttpClient, Boolean)'
{
var httpClient = apiClient.HttpClient;
}
}
public class MyAPIclient : ServiceClient<MyAPIclient>
{
protected Uri _baseUri { get; set; }
protected ServiceClientCredentials Credentials { get; set; }
public MyAPIclient(ServiceClientCredentials credentials, HttpClient client) : base(client, disposeHttpClient: false)
{
this._baseUri = new Uri("https://stackoverflow.com");
this.Credentials = credentials;
if(credentials != null)
Credentials?.InitializeServiceClient(this);
}
}
public static class HttpClientSingleton
{
public static readonly HttpClient Client = new HttpClient() { Timeout = TimeSpan.FromMinutes(30) };
}
}
}
I have a long-running WCF service hosted in a Windows service. I have a service library whose purpose is to report on the state of the service. How can I get to the instance of the service from inside an instance of the service library?
To illustrate, I created a service that records the time it started, and exposes a method to report how long it's been running. The service library needs to be able to call the service's ElapsedSeconds() method, so the the library needs a reference to the running service.
I thought I could use OperationContext.Current. Here's my service library class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Text;
using System.Threading.Tasks;
namespace TimerService
{
public class TimerServiceLib : ITimerServiceLib
{
TheTimerService m_timerService;
public TimerServiceLib()
{
var currentContext = OperationContext.Current;
var instanceContext = currentContext.InstanceContext;
m_timerService = (TheTimerService)instanceContext.GetServiceInstance();
}
public double SecondsSinceStart()
{
return m_timerService.ElapsedSeconds();
}
}
}
But the call to GetServiceInstance() creates a new instance of TimerServiceLib(), which of course gives me an infinite loop. So, what is the correct way to do this?
Here is my service class, actually being hosted in a console application:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;
namespace TimerService
{
public partial class TheTimerService : ServiceBase
{
private DateTime m_startTime;
ServiceHost m_svcHost;
public TheTimerService()
{
InitializeComponent();
Init();
m_startTime = DateTime.Now;
}
public double ElapsedSeconds()
{
return (DateTime.Now - m_startTime).TotalSeconds;
}
protected override void OnStart(string[] args)
{
}
protected override void OnStop()
{
}
public void Init()
{
if (m_svcHost != null)
{
m_svcHost.Close();
}
string httpAddress = "http://localhost:1234/TimerService";
string tcpAddress = "net.tcp://localhost:1235/TimerService";
Uri[] adrbase = { new Uri(httpAddress), new Uri(tcpAddress) };
m_svcHost = new ServiceHost(typeof(TimerServiceLib), adrbase);
ServiceMetadataBehavior mBehave = new ServiceMetadataBehavior();
m_svcHost.Description.Behaviors.Add(mBehave);
var debugBehavior = m_svcHost.Description.Behaviors.Find<ServiceDebugBehavior>();
debugBehavior.IncludeExceptionDetailInFaults = true;
BasicHttpBinding httpBinding = new BasicHttpBinding();
m_svcHost.AddServiceEndpoint(typeof(ITimerServiceLib), httpBinding, httpAddress);
m_svcHost.AddServiceEndpoint(typeof(IMetadataExchange),
MetadataExchangeBindings.CreateMexHttpBinding(), "mex");
NetTcpBinding tcpBinding = new NetTcpBinding();
m_svcHost.AddServiceEndpoint(typeof(ITimerServiceLib), tcpBinding, tcpAddress);
m_svcHost.AddServiceEndpoint(typeof(IMetadataExchange),
MetadataExchangeBindings.CreateMexTcpBinding(), "mex");
m_svcHost.Open();
// SimShopServiceLib.m_shop = new CSimShopManager();
}
}
}
You can do it this way:
First, modify your TimerServiceLib and do the constructor injection of TheTimerService:
public class TimerServiceLib : ITimerServiceLib
{
private readonly TheTimerService m_timerService;
public TimerServiceLib(TheTimerService theTimerService)
{
m_timerService = theTimerService;
}
public double SecondsSinceStart()
{
return m_timerService.ElapsedSeconds();
}
}
Then, in your Init() upon creation of ServiceHost, instantiate first your service and pass the TheTimerService. Since your are creating the ServiceHost inside your windows service, wich is TheTimerService you can pass this.
Uri[] adrbase = { new Uri(httpAddress), new Uri(tcpAddress) };
var timerServiceLib = new TimerServiceLib(this)
m_svcHost = new ServiceHost(timerServiceLib , adrbase);
For more details about passing object in you service see this link.
Disclaimer : The above code is not tested.
I'm new at .NET remoting and C#. I need a client/server application and want to handle this with .NET Remoting. I've wrote a class library for the remoting object, the EchoServer class, with some test methods.
The class library I've added to my server project in Visual Studio. The assembly "System.Runtime.Remoting" I've added, too.
The following is the code of my server:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using Remoting; //Namespace Lib
namespace Server
{
public partial class Server : Form
{
public Server()
{
InitializeComponent();
TcpChannel serverChannel = null;
try
{
serverChannel = new TcpChannel(9998);
lvStatus.Items.Add("Server is listening on port 8089...");
string strIn = "";
ChannelServices.RegisterChannel(serverChannel, true);
RemotingConfiguration.RegisterWellKnownServiceType(Type.GetType("Remoting.EchoServer, remoting_dll"), "Echo", WellKnownObjectMode.SingleCall);
}
catch (Exception ex)
{
ChannelServices.UnregisterChannel(serverChannel);
MessageBox.Show(ex.Message.ToString());
}
}
}
}
If I start the server, I will get an exception:
The value cannot be NULL
Parametername: type
I've tried some other code of a tutorial, but I will get the same excetion, equal if the class for the remoting object is implented as a class library or it is as a class directly in my project.
Can you post implementation of Remoting?
I thing that your mistake is next:
"Remoting.EchoServer, remoting_dll"
So, you should use Type.GetType correctly.
Example of working code:
static void Main(string[] args)
{
Server();
}
static void Server()
{
Console.WriteLine("Server started...");
var httpChannel = new HttpChannel(9998);
ChannelServices.RegisterChannel(httpChannel);
RemotingConfiguration.RegisterWellKnownServiceType(Type.GetType("Server.Program+SomeClass"), "SomeClass", WellKnownObjectMode.SingleCall);
Console.WriteLine("Press ENTER to quit");
Console.ReadLine();
}
public interface ISomeInterface
{
string GetString();
}
public class SomeClass : MarshalByRefObject, ISomeInterface
{
public string GetString()
{
const string tempString = "ServerString";
Console.WriteLine("Server string is sended: {0}", tempString);
return tempString;
}
}