29

Background: I am working on a project that involves a WinForms app. The client wants to expose a local-only HTTP server to allow other apps to trigger functionality on a running instance of the WinForms app via a REST API (or similar). The preference is to implement the aforementioned API using ASP.NET Core.

My question is thus: How do I structure a project to have both an ASP.NET Core API and a WinForms GUI in the same process? Are there any pitfalls I'd have to be wary of?

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
Matthew King
  • 5,114
  • 4
  • 36
  • 50
  • Why do you want to run both programs in the same process? – citronas Feb 03 '20 at 09:22
  • @citronas *to allow other apps to trigger functionality on a running instance of the WinForms app via a REST API*, Basically an inter-process communication. – Reza Aghaei Feb 03 '20 at 09:41
  • @Matthew Questions which start with "*What is the best way ...*" are usually off-topic because they are opinion based. Do you have any specific question about a specific problem? – Reza Aghaei Feb 03 '20 at 09:46
  • @RezaAghaei Sorry, replace "what is the best way" with "how do I"... Although, given that the vast majority of programming questions have more than one solution I'd argue that almost every question has an implicit "what is the best way" component :-) – Matthew King Feb 03 '20 at 11:32
  • @citronas Exactly as Reza Aghaei answered - IPC. And it's not so much "both programs" as it is two different interfaces to the same program. – Matthew King Feb 03 '20 at 11:34
  • Doing this makes no sense. Even if you run both under the same process... What kind of "interaction" would it have between them? This breaks all sorts of architecture principals and best practices. Encapsulation, Single Responsibility, Layer Design, proper tier design. – Jonathan Alfaro Feb 03 '20 at 20:55
  • As for pitfalls.... The threading model are probably very different so things might not end up working as you might expect. – Jonathan Alfaro Feb 03 '20 at 20:55
  • @JonathanAlfaro Just to clarify, the client wants to do it to facilitate one-way IPC between the main app and a number of "separate but related" apps. It's not my first choice for IPC, but it's not exactly breaking any more architectural principals than any other method of IPC, either. Can you elaborate please? – Matthew King Feb 03 '20 at 22:30

3 Answers3

50

Hosting ASP.NET CORE API in a Windows Forms Application and Interaction with Form

Here is a basic step by step example about how to create a project to host ASP.NET CORE API inside a Windows Forms Application and perform some interaction with Form.

To do so, follow these steps:

  1. Create a Windows Forms Application name it MyWinFormsApp

  2. Open Form1 in design mode and drop a TextBox on it.

  3. Change the Modifiers property of the textBox1 in designer to Public and save it.

  4. Install Microsoft.AspNetCore.Mvc package

  5. Install Microsoft.AspNetCore package

  6. Create a Startup.cs file in the root of the project, and copy the following code:

     using Microsoft.AspNetCore.Builder;
     using Microsoft.AspNetCore.Hosting;
     using Microsoft.AspNetCore.Mvc;
     using Microsoft.Extensions.Configuration;
     using Microsoft.Extensions.DependencyInjection;
     namespace MyWinFormsApp
     {
         public class Startup
         {
             public Startup(IConfiguration configuration)
             {
                 Configuration = configuration;
             }
             public IConfiguration Configuration { get; }
             public void ConfigureServices(IServiceCollection services)
             {
                 services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
             }
             public void Configure(IApplicationBuilder app, IHostingEnvironment env)
             {
                 if (env.IsDevelopment())
                 {
                     app.UseDeveloperExceptionPage();
                 }
                 app.UseMvc();
             }
         }
     }
    
  7. Copy the following code in Program.cs:

     using System;
     using System.Threading;
     using System.Windows.Forms;
     using Microsoft.AspNetCore;
     using Microsoft.AspNetCore.Hosting;
    
     namespace MyWinFormsApp
     {
         public class Program
         {
             public static Form1 MainForm { get; private set; }
    
             [STAThread]
             public static void Main(string[] args)
             {
                 CreateWebHostBuilder(args).Build().RunAsync();
    
                 Application.EnableVisualStyles();
                 Application.SetCompatibleTextRenderingDefault(false);
                 MainForm = new Form1();
                 Application.Run(MainForm);
             }
    
             public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
                 WebHost.CreateDefaultBuilder(args)
                     .UseStartup<Startup>();
         }
     }
    
  8. Create a folder called Controllers in the root of the project.

  9. Create ValuesController.cs in the Controllers folder and copy the following code to file:

     using System;
     using Microsoft.AspNetCore.Mvc;
    
     namespace MyWinFormsApp.Controllers
     {
         [Route("api/[controller]")]
         [ApiController]
         public class ValuesController : ControllerBase
         {
             [HttpGet]
             public ActionResult<string> Get()
             {
                 string text = "";
                 Program.MainForm.Invoke(new Action(() =>
                 {
                     text = Program.MainForm.textBox1.Text;
                 }));
                 return text;
             }
    
             [HttpGet("{id}")]
             public ActionResult Get(string id)
             {
                 Program.MainForm.Invoke(new Action(() =>
                 {
                     Program.MainForm.textBox1.Text = id;
                 }));
                 return Ok();
             }
         }
     }
    
  10. Run the application.

  11. Type "hi" in the textBox1

  12. Open browser and browse http://localhost:5000/api/values → You will see hi as response.

  13. http://localhost:5000/api/values/bye → You will see bye in textBox1

Further Reading

You may also be interested in How to use Dependency Injection (DI) in Windows Forms (WinForms)

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • The example uses GET methods just to keep thing simple for test in browser. You may want to use different verbs based on your requirements. – Reza Aghaei Feb 03 '20 at 20:39
  • This is not about best practices, it's about quick start and a simple PoC showing what you can do. – Reza Aghaei Feb 03 '20 at 20:47
  • Thanks Reza. Do you know if this approach will cause any issues with the threading model? – Matthew King Feb 03 '20 at 22:32
  • 3
    As I mentioned in the comments, the answer is not about best practices, it's more a PoC, however there are some points about this solutions: (1) In general, the idea is OK, it's inter-process communication. (2) I have used a similar approach using WCF for IPC between WinForms applications in a production environment without any problem. (3) I used ASP.NET WEB API for this purpose in test environments, but I haven't used ASP.NET CORE for this purpose yet. (4) As long as you use Invoke to access UI thread, there is no problem to try to access to UI thread. – Reza Aghaei Feb 04 '20 at 07:55
  • Another option to consider, you can think of using MSMQ or any other message bus to communicate between the applications in an event/message based solution. (which is out of scope of this post) – Reza Aghaei Feb 04 '20 at 07:56
  • If I gave it more try, I'll let you :) – Reza Aghaei Feb 05 '20 at 05:10
  • 2
    Thank you Reza, your answer works like a charm. – morteza jafari Nov 26 '21 at 16:23
  • @mortezajafari Awesome! Glad to hear that worked for you! – Reza Aghaei Nov 27 '21 at 23:10
  • When its an async method which also returns a result use: test = Program.MainForm.Invoke(new Func>(async () => { return await Task.FromResult(await Program.MainForm.ParseOrderbookAsync()); })).Result; – Jan Oct 03 '22 at 14:01
  • Thanks for your code, but not support NuGet packages higher than 2, System.MethodAccessException: 'Attempt by method 'Microsoft.Extensions.Logging.Configuration issue – Zanyar Jalal Oct 21 '22 at 16:43
  • @ZanyarJ.Ahmed I cannot remember the version of .NET which I used for this code. But I'll probably update the steps for .NET 6 once I find time. And the packages are the same package as you use in a ASP.NET CORE project. – Reza Aghaei Oct 21 '22 at 17:08
  • @Ruyut updated for .NET 6 – Zanyar Jalal Oct 21 '22 at 17:11
1

I Install Microsoft.AspNetCore.Mvc and Microsoft.AspNetCore package can't use

I look at this WebApplication.CreateBuilder Method Doc find need Microsoft.AspNetCore.dll, but I can't use this.

I hope it helps others.


Install Microsoft.AspNetCore.App package

dotnet add package Microsoft.AspNetCore.App --version 2.2.8

the Minimal API:

Microsoft.AspNetCore.Builder.WebApplication app = Microsoft.AspNetCore.Builder.WebApplication.Create(new string[] { });
app.MapGet("/", () => "Hello World!");
app.RunAsync();

Ok, now you can open http:localhost:5000 to look Hello World!

Because few people will use parameters in WinForm, string[] args is omitted here, and new string[] { } is used instead


If you want to ues controller, like ASP.NET Core, then you can use this:

var builder = Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(new string[]{});
builder.Services.AddControllers();

var app = builder.Build();
app.MapControllers();
app.RunAsync();

HomeController.cs

using Microsoft.AspNetCore.Mvc;

namespace RuyutWinFormsApi;

[Route("api/[controller]")]
[ApiController]
public class HomeController : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok("This is a test api");
    }
}

you can open http://localhost:5000/api/home to look This is a test api

BTY, if you want to change port, you can add this:

app.Urls.Add("http://0.0.0.0:8080");
Ruyut
  • 151
  • 11
1

I have a class I made to do this. Basically I created a function in the .NET 6.0 Web API Controller starting template.

public async Task Listen(IServiceCollection services, List<Type> controllers, int port, CancellationToken token)
{
    var builder = WebApplication.CreateBuilder();
    builder.WebHost.ConfigureKestrel(options => options.Listen(System.Net.IPAddress.Parse("127.0.0.1"), port));
    // Add services to the container.

    builder.Services.AddControllers();
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();

    builder.Services.Add(services);
    //builder.Services.AddSingleton<HostedCheckoutService>(hc);

    // Add our external card processor controller. This mocks a web page that is returned
    // in the Hosted Checkout iframe.
    //var assembly = typeof(LocalCardProcessorController).Assembly;
    foreach (var controller in controllers)
    {
        var assembly = controller.Assembly;
        builder.Services.AddControllers().PartManager.ApplicationParts.Add(new Microsoft.AspNetCore.Mvc.ApplicationParts.AssemblyPart(assembly));
    }

    var app = builder.Build();

    //Configure the HTTP request pipeline.
    if (true)//app.Environment.IsDevelopment())
    {
        // Need to manually navigate to http://127.0.0.1:16600/swagger/index.html
        app.UseDeveloperExceptionPage();
        app.UseSwagger();
        app.UseSwaggerUI(c => c.SwaggerEndpoint("v1/swagger.json", "MyAPI V1"));
    }

    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints => endpoints.MapControllers());
           
    await app.RunAsync(token);
    await app.DisposeAsync();

}

Call it in your form like so;

APIServer server = new APIServer();

var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<SomeService1>();
serviceCollection.AddSingleton<SomeService2>();
server.Listen(serviceCollection, new() { typeof(SomeController1), typeof(SomeController2) }, 9001, new CancellationToken());

Note Swagger is using Swashbuckle.AspNetCore (6.3.0)

clamchoda
  • 4,411
  • 2
  • 36
  • 74