Links
Tags
Secondary IP Addresses for EC2 Instances using Amazon Cloud Development Kit
Amazon CDK significantly reduces the effort to build and maintain CloudFormation templates where many intricate activities and connected resources are required, however I find myself constantly at battle with Level 2 constructs not supporting some - what I'd consider - fairly fundamental configurations particularly for configuring EC2 instances in this nature.
Today's roadblock was around provisioning EC2 instances using the L2 Construct to provision additional secondary IP addresses in a way that won't cause drift, or falling back to a total-Cfn* L1 Constructs to build EC2 instances.
Please note that this is a super-hacky way of achieving this and until such time that issue #19326 is resolved, this appears to be my only workaround without breaking the connections attribute for further attaching Security Groups or breaking network connectivity.
const ec2: cdk.aws_ec2.Instance = { ... } // your configuration here
// create a NetworkInterfaceProperty resource. This must have deviceIndex = '0' and point to your subnet / security group
const networkInterfaceProperty: cdk.aws_ec2.CfnInstance.NetworkInterfaceProperty = {
deviceIndex: '0',
description: 'Custom ENI Configuration with multiple Secondary IP addresses',
secondaryPrivateIpAddressCount: 2,
subnetId: ec2.instance.subnetId,
groupSet: ec2.instance.securityGroupIds
};
// CloudFormation will not allow subnet IDs and Security Groups to be set both at the AWS::EC2::Instance level and your NetworkInterfaces property, so we must clear those out to ensure they are not output in CloudFormation. This does keep the L2 construct helpers (such as connections) working as intended.
ec2.instance.subnetId = undefined;
ec2.instance.securityGroupIds = undefined;
// Finally, attach the NetworkInterfaceProperty in an array.
ec2.instance.networkInterfaces = [ networkInterfaceProperty ];
Be mindful if that it was this simple, I'm sure that issue would have been addressed by now - so I'm sure there are many problems with this approach if you were to extend it beyond the example given here. However, this should be a good starting point if you want to use L2 Constructs for convenience but maintain some customisation without fully resorting to Cfn* L1 Constructs.
And... that's it! Short and sweet.
MediatR vs. Minimal APIs vs. FastEndpoints
In the world of .NET-based microservice development, no doubt you have come across the de-facto standard of all-things-CQRS - MediatR. It has been a great library to work with when your application stack contains multiple entry points to the same unit of code, and you really don't want to spend the time writing boilerplate reflection code to achieve similar outcomes. In some respects, it's become a bit of a hammer for me - download the library, add the references and start writing your request / response objects just as I've been writing since the first "simple" WCF replacements became available for REST. It's hard to move away from the simplicity of treating things as Requests, using some scaffolding to find the right handler and produce a Response. It's simple, it follows the way HTTP works, there's just not a lot to think about.
In .NET 6, there's been a push by Microsoft to start introducing minimal code. I was rudely introduced to this in a Console App I launched a few months ago when old mates public static void Main(string args[]) and using System; were all missing from my code. No namespace, no nothing. Just an empty screen with Console.WriteLine("Hello World");. Delving a little further, it appears that ASP.Net wasn't immune from the same smack-in-the-face change either. Just 4 lines of boilerplate code to instantiate your website removing slabs of Startup.cs and Program.cs code.
Embracing this new life of simplicity inching ever-so-much closer to the conveniences of Python (Fast) and NodeJS (expressjs), .NET 6 and a reasonably newcomer to the platform FastEndpoints might well become the second and third hammers I need to take the stickiness out of several layers of abstraction that MediatR and MVC usually requires before you can use it as intended.
A brief look at Minimal APIs
When you create your new empty ASP.Net Core website, you'll be greeted with a new option "Do not use top-level statements". This option removes the boilerplate / scaffolding code for using statements, Program.cs, Startup.cs and lets you get down to business almost immediately.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
This is significantly less code than how you'd do this in MVC. I won't put an example here, but the above demonstrates the following:
- Minimal APIs is initialised by default (there's no code here to activate MVC Controllers or Routes).
- Routes are explicitly defined against their appropriate MapVerb (and thus not implied due to naming conventions or altered via Attributes).
- No Namespaces or Class Decorators - your code should run a lot faster than initiating large Controller classes.
Getting started is simple - create some DTO objects to match your requests and responses, write minimal code here and gradually update them as you go on. If you're really embracing the microservices design, you might even find that a single Program.cs is enough for your few data manipulation services. Getting more complicated? Dependency Injection is still available, as is reflection or simple instantiation of your own abstractions will help you get there.
You can still instantiate controllers and use them where appropriate, but when working with APIs, I've never found API Controllers to be particularly intuitive. There's a lot going on simply to handle authorisation and routing, and if you're embracing MediatR / CQRS patterns - you're controllers become nothing more than conduits for mediator Send calls.
What is FastEndpoints?
FastEndpoints is the not-so-new kid on the block embracing the concept of Minimal APIs by allowing you to replace those MVC Controllers with Endpoints. When paired with "Do not use top-level statements", instantiation is super simple. You'll first need a couple of packages:
With those installed, we add the scaffolding and we're ready to write our first Endpoint.
global using FastEndpoints;
global using FastEndpoints.Swagger;
// do the services thing here
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFastEndpoints();
builder.Services.AddSwaggerDoc();
// including things like your DI
var app = builder.Build();
// do the app thing here
app.UseAuthorization();
app.UseFastEndpoints();
app.UseOpenApi();
app.UseSwaggerUi3(s => s.ConfigureDefaults());
// start!
app.Run();
Instead of the Minimal APIs approach with app.MapGet("/"), you create a typical class extending one of the following base classes:-
- Endpoint<TRequest> or Endpoint<TRequest, TResponse>
- EndpointWithoutRequest or EndpointWithoutRequest<TResponse>
Your TRequest and TResponse objects are Plain Old Class Objects. With this in mind, you can start scaffolding your class.
// usings for your POCO's
namespace App.Endpoints
{
public class UpdatePocoEndpoint : Endpoint<PocoRequest, PocoResponse>
{
public override void Configure()
{
AllowAnonymous();
Post("/poco");
Summary(s =>
{
// put your swagger configurations here
// per FastEndpoints.Swagger
});
}
public override Task HandleAsync(PocoRequest req, CancellationToken ct)
{
// do something to update your Poco
return SendOkAsync(new PocoResponse(), ct);
}
}
}
The first thing you notice is the Configure() method - this creates a semantic way of declaring the configuration. These also ways to use Attributes to achieve the same thing. More importantly, there's no expensive MVC framework, DI to be instantiated for all 'routes' and really - you're just focussing on your code.
If you're building simple request / response style APIs, give this a try - you'll remove a lot of unnecessary code while still supporting Dependency Injection through either your Resolve extensions or via your constructor as you do today.
For more details, see the comprehensive documentation on FastEndpoints.
What about MediatR?
While it's not the cleanest pattern out there, I'm a fan of MediatR. While it masquerades as the original Gang of Four 'Mediator' pattern, it really is a glorified service locator pattern useful for mapping a series of requests and responses. In doing so, it's a useful receptacle for both Request / Response and CQRS style APIs when coupled with ASP.Net MVC Web APIs where your ApiControllers masquerade as some kind of gateway.
Once you add your MediatR library (and DependencyInjection component) via NuGet to your ASP.Net MVC Web API project, you then add MediatR to your ConfigureServices method in Startup.cs
services.AddMediatR(typeof(Startup));
In a roundabout way, MediatR extracts the executing assembly from your injected Type, and begins the hunt for things that implement:
- IRequest
- IRequest<TResponse>
- IRequestHandler<TRequest, TResponse>
That means you'll need a few components:
// queries - specify your repsonse type
public class PocoQuery : IRequest<Poco> {
// stuff for your handler request
}
// commands (if dealing with CQRS, you don't want to return anything
public class AddPocoCommand : IRequest {
// add your properties here
}
// query handlers
public class PocoQueryHandler : IRequestHandler<PocoQuery, Poco> {
// add your di and constructors
public async Task<Poco> Handle(PocoQuery query, CancellationToken cancellationToken) {
// return a Poco here.
}
}
// command handlers
public class AddPocoCommandHandler : IRequestHandler<AddPocoCommand> {
// add your di and constructors
public async Task Handle(AddPocoCommand command, CancellationToken cancellationToken) {
// do your writes and publishes here.
}
}
This is where the "magic" reveals itself. All of those interfaces that you extend - and that's assuming you don't go further at creating base classes to mimic "Command" and "Query" nomenclature (e.g. public interface ICommand : IRequest { ... } and so forth). I don't mind it personally because it moves the 'mess' of Dependency Injection over to your commands - but it doesn't make it very portable, rather it tightly couples your library to the MediatR framework. Again - this is a tradeoff. If you are happy to embed MediatR into your application then you should make full use of it. It is unit testable and it can make things logically cleaner. But it's not magic.
It also makes code discoverability very tricky without third party plugins, or an aptitude in "ctrl+," exploring via naming conventions. You can also go searching for 'references in other locations' - it's still not as simple as finding the concrete implementation of an interface as you would with Dependency Injection.
Why not MediatR?
Some arguments for using MediatR is not having to write your own handlers or lists, but being totally honest - a competent developer will hammer that out as part of scaffolding. It's not an overly complicated task, and making architecture decisions like that means you need to be totally on-board with what the library offers. If you were to write your own Query Handler interface, one may assume it could look as simple as:
// query handler
public interface IQueryHandler<TQuery, TResult> {
public abstract Task<TResult> ExecuteAsync(TQuery query, CancellationToken token);
}
// command handler
public interface ICommandHandler<TCommand> {
public abstract Task ExecuteAsync(TCommand command, CancellationToken token);
}
You then register these in your Dependency Injection framework as follows:
// in your Startup.cs
services.AddScope<IQueryHandler<MyQuery, MyQueryResult>, QueryHandler<MyQuery, MyQueryResult>>();
// in your constructor
public class SomeWebController : ApiController {
readonly IQueryHandler<MyQuery, MyQueryResult> handler;
public SomeWebController(IQueryHandler<MyQuery, MyQueryResult>) { ... }
}
You can certainly see why MediatR can seem nice at first - there's less "stuff" going on, however all those handler registrations - all those interfaces you add to your commands and queries, and pointless abstractions on-top mean you end up writing the same amount of code anyway - and you don't get the same level of visibility.
If you don't want to specify all those types, you can proceed to add extras to your interfaces, and then not bind service locator patterns in your code.
If you want to automatically find all of your query handlers, then reflection can help you write a method to do exactly that - just as the MediatR "typeof(Startup)" component works.
That's where FastEndpoints may have an advantage here. Going back to why we use MediatR in our Web APIs, might have something to do with the way routing in MVC works. You tend to add lots of entrypoints - kind of like an API Gateway endpoint meaning your constructor could be totally bloated with handlers for each of your methods. That would be undesirable and messy.
However, if FastEndpoints allows you to specify your routing path (without having to instantiate full ApiControllers and adding lots of attributes to change route paths), then your DI spam will be reduced. In fact, if you need to share code or write directly into your endpoint - you'll find less lines of code and better discoverability to achieve the same.
Will I stop using MediatR? Yes - I've stopped using it for my personal projects and switched to FastEndpoints. Those also have different use cases though (e.g. if you make a Christmas Light controller - you have ~15 endpoints - not ~1,500). Either way, I'm looking forward to what Minimal APIs has to offer in upcoming releases and how much Microsoft may borrow from projects like FastEndpoints to create that experience enjoyed by node.js and Python web developers in 2022.