The Microsoft ecosystem is not kind to Ruby developers. The Ruby SDK for Azure was retired in February 2021, and the support for Ruby in the OpenAPI client generator Kiota is extremely limited, if not unusable. However, .NET Aspire is a special case. This ambitious local development orchestrator is not tied to any specific technology, as I explained in my previous article on the inner workings of .NET Aspire. Therefore, it is possible to run Ruby on Rails web applications on it.
Please note, the source code presented in this article is subject to change, as .NET Aspire is still under development.
Prerequisites
- .NET SDK and .NET Aspire must be installed (latest version at the time of this article: preview 3),
- Ruby 3.x (for Windows, use RubyInstaller),
- Ruby on Rails.
Developing a custom Rails resource for .NET Aspire's app model
.NET Aspire uses an "app model", which represents the list of resources that make up a distributed application. The two primary resource types in .NET Aspire are executables and containers. For Ruby on Rails web applications, we want to create an executable with the following command:
ruby bin/rails server
This command is used to start a Rails application and is cross-platform. Thanks to the extensibility of the .NET Aspire app model, we can define our own Rails resource, which inherits from the ExecutableResource
class in .NET Aspire. Here is the complete source code for declaring a Rails application in .NET Aspire:
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
namespace Aspire.Hosting;
internal class RailsAppResource(string name, string command, string workingDirectory, string[] args)
: ExecutableResource(name, "ruby", workingDirectory, ["bin/rails", command, .. args]);
internal static class RailsAppExtensions
{
public static IResourceBuilder<RailsAppResource> AddRailsApp(
this IDistributedApplicationBuilder builder, string name, string command, string workingDirectory, string[]? args = null)
{
var resource = new RailsAppResource(name, command, workingDirectory, args ?? []);
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IDistributedApplicationLifecycleHook, RailsAppAddPortLifecycleHook>());
return builder.AddResource(resource)
.WithOtlpExporter()
.WithEnvironment("RAILS_ENV", builder.Environment.IsDevelopment() ? "development" : "production")
.ExcludeFromManifest();
}
}
internal sealed class RailsAppAddPortLifecycleHook : IDistributedApplicationLifecycleHook
{
public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var railsApps = appModel.Resources.OfType<RailsAppResource>();
foreach (var railsApp in railsApps)
{
if (railsApp.TryGetEndpoints(out var endpoints))
{
var envAnnotation = CreateAddPortEnvironmentCallbackAnnotation(endpoints.ToArray(), railsApp);
railsApp.Annotations.Add(envAnnotation);
}
}
return Task.CompletedTask;
}
private static EnvironmentCallbackAnnotation CreateAddPortEnvironmentCallbackAnnotation(IReadOnlyCollection<EndpointAnnotation> endpoints, IResource app)
{
return new EnvironmentCallbackAnnotation(env =>
{
var hasManyEndpoints = endpoints.Count > 1;
if (hasManyEndpoints)
{
foreach (var endpoint in endpoints)
{
var serviceName = hasManyEndpoints ? $"{app.Name}_{endpoint.Name}" : app.Name;
env[$"PORT_{endpoint.Name.ToUpperInvariant()}"] = $"{{{{- portForServing \"{serviceName}\" -}}}}";
}
}
else
{
env["PORT"] = $"{{{{- portForServing \"{app.Name}\" -}}}}";
}
});
}
}
Let's focus on the experience of declaring a Rails application in your Program.cs file of your .NET Aspire project:
var builder = DistributedApplication.CreateBuilder(args);
builder.AddRailsApp("myrailsapp", "server", "path/to/your/rails/app")
.WithEndpoint(hostPort: 3000, scheme: "http");
builder.Build().Run();
The need to create a lifecycle hook for our Rails resources is due to the internal networking operations in .NET Aspire. This internal operation is explained in detail in the official documentation: .NET Aspire inner-loop networking overview.
Top comments (0)