Wednesday, October 21, 2020

Six Lessons Learned and a New Tool Published After Organizing My 1st Virtual CodeCamp

6 lessons learned while moving the NoVA Code Camp online, and announcing Sessionized, a new open source app for Code Camp organizers

2020 hasn't generally been an awesome experience, fortunately organizing NoVA CodeCamp in a post-COVID world was.  I met wonderful people, gained an appreciation for all conference organizers, and felt like I helped the developer community grow.  

In this post I'll share what I learned organizing an event for my first time ever, introduce an open source tool I just published for other organizers, and hopefully provide information to help decrease stress for others figuring out how to move forward in a post-COVID world.

1. Expect More Response in Call for Speakers

NoVA Code Camp has used Sessionize for a long time.  It's a great tool for capturing and evaluating presentations.  It costs nothing for free conferences like ours, is great for communicating with speakers, and as I'll explain shortly: has an awesome API.

During call for speakers we received 75 fantastic submissions from a variety of locations and time zones (not just Northern Virginia).  75 was about 50% more than usual thanks to being online.  However, reading through 75 presentations turned out to be a lot of work and I wish I'd started earlier and known to put extra time into the schedule for evaluation.


2. Plan Shorter Sessions

Historically our sessions have been 75 minutes each, which equates to 5 sessions in a track.  With 37 speakers we would have need over 7 tracks!  We only had 4 volunteers, so that simply wasn't going to work.

At the same time, I was interviewing several experienced online organizers to get tips and tricks (thank you Alex SlotteJim Novak, and of course Ed Snider the main organizer for NoVA Code Camp in years past).  One individual suggested that online audiences had a shorter attention span and that sessions should be shorter.

Voila, with a little culling (not easy!), and a reduction to 60 minutes (45 for presentations + 10 minutes for Q&A + 5 for a coffee break) we made it all fit.  I have no data to prove the shorter attention span theory, but the conference timing seemed to work out nicely.


3. Create Speaker Cards

Marketing an online event is substantially different from marketing an in-person one.  Once call for speakers closed we did the normal things: e-mailing previous attendees, notifying leaders of nearby meetups, and creating a registration page on Eventbrite (which also has no cost for free conferences).  That achieved a moderate response.

Then one of our speakers, Gant Laborde, who speaks regularly, made the suggestion of "speaker cards" to help the speakers to promote the event like he'd seen at a react conference.


Sounded great, but the thought of making 27 images in Paint.Net was cringy (as my daughter would say), and so (of course) I wrote an app instead.  It pulled Sessionize data from their API, provided text for the 27 tweets with speaker twitter handles, and linked to a "speaker page".  


The speaker page it linked to provided a picture, presentation description, speaker bio, and a link to our Eventbrite registration page.  Then I just took screenshots of each speaker page, copied the tweets, and pasted them all into Buffer for posting slowly over many days.

It worked beautifully.  The speakers loved it, it helped them promote, and we ended up with nearly 200 registered attendees by the day of.  Incidentally, that 200 translated to 70 at peak, which is a good showing historically:


4. Allow Audience Interaction

Audience interaction is a critical element of a Code Camp, and conferences generally.  But how to reproduce that in-person feel while preventing unsavory individuals from doing inappropriate things?

We chose Discord to solve that problem.  It allowed attendees to interact with each other and the speakers and the organizers.  And it has automated tools for spotting inappropriate content.  We additionally posted a code of conduct and had one of our organizers (Stan Reiser) who was an expert with Discord set up permissions and train everyone on how to mute and boot.

5. Perform 1-1 AV Checks For Everyone

Personalized one-on-one AV checks for each speaker probably saved the conference.

We'd settled on Microsoft Teams for the main event after much research and deliberation because it was inexpensive and their "Live Events" feature looked awesome for attendees.

Live Events allowed attendees to easily join without authenticating, provided Tivo-like features of pause and rewind, was mobile friendly, recorded presentations for later viewing or posting to YouTube (see also our new NoVA Code Camp YouTube Channel), and provided a moderated Q&A.  


Sounded great, but not everyone loved Teams.  While the experience was excellent for attendees, getting speakers logged in as guests accounts in our single-user Office 365 account was finnicky.  The good news for subsequent organizers: we talked with a Microsoft employee who said these were known issues and will be resolved in the next update.

Regardless, the number one piece of advice I can offer other organizers: leave a couple weeks in the schedule to do AV checks for every single speaker.  27 AV checks would be too much for any one person to do while staying employed.  Fortunately, we assigned one moderator as MC for each channel so no one person had to do more than 7 AV checks.

In the check we ensured everyone could get in (the hard part), their audio sounded crisp and clean, the moderator pronounced their name correctly, they understood the 10 second delay, and understood how to work the moderated Q&A.

6. Sell Your Speakers During the Event (Announcing, Sessionized!)

Everything seemed to be in order for the big day, but one last concern: Having attendees navigate through 27 presentations to pick which ones to attend could be intimidating.  And how to keep people engaged after a talk ended?

The default grid that Sessionize provides (pictured earlier in green) was ok, but it isn't especially pretty or usable.  It also doesn't provide links to "watch now".  It wasn't going to cut it for an online conference.

Fortunately implementing per-track and per-time slot views with links to speaker details pages with the app I'd already started only took a couple more evenings:



Oh, and the moderators also asked for slides to show when out to lunch or if there were technical difficulties.


But fortunately we never had to show that last screen.  The CodeCamp went off without a hitch.  Thanks to a great team (Scott LockLaBrina Loving, and Stan Reiser) and a lot of hard work up-front, all of the speakers got in, none of the Internet's unsavory characters did, and a good time was had by all😁.

And since it wasn't much extra effort to genericise and open source the tool, why not?  Thus I just published sessionized.  I hope it helps another CodeCamp organizer somewhere.  You can view NoVA Code Camp's implementation here.

Summary

2020 has brought many changes, most of them not so great.  However, for Code Camp I think our changes were mostly positive.  Of course I missed the in-person interaction (and the cronuts), but moving online brought us our first international speaker (thank you Emanuil), the coffee was better (no offense Stan), we had a long time volunteer turned first time speaker (Go Vijay!), a new volunteer (yayy LaBrina!), and many excellent first-time and returning speakers to NoVA Code Camp (thank you all!).

Overall we had an excellent Saturday of fun, learning, and networking all while staying safe.  There was much rejoicing.  And if you're an organizer I hope this post will help you find your way to much rejoicing at your next event.

Tuesday, August 18, 2020

Deploying ASP.Net Boilerplate Apps for Devs in a Hurry


I've deployed two apps based on ASP.Net Boilerplate (Angular + ASP.Net Core) through to production.  The second used Azure Kubernetes Services.  It's slick, sophisticated, self-healing, extremely scalable, uses infrastructure as code with a multi-stage Azure DevOps build pipeline, and it's micro-services are independently deploy-able.

That second site is awesome.  It's also complicated, took a lot of effort to build, and will take a lot of effort to maintain.  This is not its story.


This is the story of my first site, where I threw the SPA (Single Page Application) into ASP.Net Core's wwwroot directory, and slung it up to Azure App Services as a single site, and called it a day.  Kind of mostly.

Anyway, it didn't need micro-services, massive scalability, self-healing, and all that mumbo jumbo.  It just needed to be simple, quick to build, and easy to maintain.

If deploying a SPA in an ASP.Net Core site is in your future, you're in a hurry, and quick and easy sounds good: this article is for you.  I'll go over it in five steps:

  1. Create Database
  2. Deploy Database
  3. Create App Service
  4. Compile and Deploy Host
  5. Compile and Deploy Angular

If you're already familiar with deploying Azure Resources via the Azure CLI skip to step 5.  If you'd like to watch this as a video check out E31 of Code Hour.


Otherwise, here's the how to create and deploy starting from zero, optimizing for simplicity:

1. Create Database

Obviously we need a database.  Using the Azure Portal is wonderful, but let's say we want repeatability.  Perhaps for other environments (e.g. test/prod), or for subsequent projects.  So let's use a script.

But how to avoid the problem of running it twice and creating duplicate resources?  Parameters, right?  And if statements to ensure the resource doesn't already exist.

But what if we want to make a resource change, e.g. to the database SKU, and deploy it through to multiple environments?  This is sounding complicated, and it's what products like Terraform are made for.  With Terraform we can describe the desired state using configuration as code and the tool looks at actual state and makes it happen, like magic.

Terraform sounds awesome (it is), but let's avoid 3rd party dependencies and not worry about idempotence at all for now.  I'm also sorely tempted to use Cake and the world famous (not really) Cake.Azure CLI Plugin, but we're in a hurry, let's avoid all nonessential 3rd party dependencies 😒.

# todo: Use Terraform
# todo: Convert this all to Cake for dependency management

$resourceGroupName = "LeesStoreQuickDeploy"
$location = "eastus"

# login to Azure interactively, fyi this won't work in scripts
az login

# create a resource group to hold everything
az group create -l $location -n $resourceGroupName

# create a sql server
$sqlUsername = "[username]"
$sqlPassword = "[password]"
$sqlName = "leesstorequickdeploy"
az sql server create -g $resourceGroupName -n $sqlName `
    -u $sqlUsername -p $sqlPassword -l $location

# create a sql database
$sqlDbName = "LeesStoreQuickDeploy"
az sql db create -g $resourceGroupName -s $sqlName -n $sqlDbName `
    --compute-model "Serverless" -e "GeneralPurpose" -f "Gen5" `
    -c 1 --max-size "1GB"


2. Deploy Database

Obviously we need to get the database tables created and insert some data.  That's exactly what the Entity Framework Migrations are for.  

It'd be tempting to update appsetting.json with the connection string info from step 1 and run dotnet ef database update or Update-Database just like we did locally.  However, that approach won't support the multi-tenancy multi-database scenarios granted by ASP.Net Boilerplate (see also Multi-Tenancy is Hard: ASP.Net Boilerplate Makes it Easy).  Furthermore, if we ever do a multi-stage Azure DevOps pipeline, we'd want to publish a single asset that we could download and run in each stage for each environment.

Fortunately, this is exactly why ASP.Net Boilerplate provides the "Migrator" command line application.  We just need to compile and run it.

# note: convert connection string to a parameter, or at the very least
#     don't check it in to source control (the way I did ;) )
$connectionString = "Server=leesstorequickdeploy.database.windows.net;Initial Catalog=LeesStoreQuickDeploy;UID=lee;Password=[pwd];"

# clean
Remove-Item ".\dist\Migrator\*.*"

# publish migrator (single file so we can publish it as an asset)
dotnet publish -c Release -o ".\dist\Migrator" `
    -r "win-x64" /p:PublishSingleFile=true `
    .\aspnet-core\src\LeesStore.Migrator
Copy-Item ".\aspnet-core\src\LeesStore.Migrator\log4net.config" `
    ".\dist\Migrator"

# run migrator with customized connection string
Push-Location
Set-Location ".\dist\Migrator"
$env:ConnectionStrings__Default = $connectionString
.\LeesStore.Migrator.exe -q
Pop-Location


3. Create App Service

✔ Databases & Migrations
❔ Webs of Azure

There are many options to deploy a website, even narrowing the universe to Azure.  They all have pro's and con's.  Virtual Machines (IaaS) are expensive and require maintenance.  Kubernetes is complicated.  Azure's Web App for Containers Instances is pretty awesome, and ASP.Net Boilerplate is already containerized.  But then we'd need to maintain the container image and it adds complexity.  

App Services are simple and fast, and good enough for most scenarios:

$appServicePlan = "LeesStoreServicePlan"
$webAppName = "leesstore2"
az appservice plan create -n $appServicePlan -g $resourceGroupName --sku Free -l $location
az webapp create -n $webAppName -g $resourceGroupName -p $appServicePlan


4. Compile and Deploy Host

A website without code is like a taco without fillings: an abomination.


To fix it first we have to set the connection string.  The rest should be relatively easy, just dotnet publish, compress the results, and upload them with an az command:

# set connection string
az webapp config appsettings set -n $webappname -g $resourceGroupName --settings ConnectionStrings__Default=$connectionString

# compile
dotnet publish -c Release -o ".\dist\Host" .\aspnet-core\src\LeesStore.Web.Host

# zip
Compress-Archive -PassThru -Path ".\dist\Host\*" -DestinationPath ".\dist\Host.zip"

# upload
az webapp deployment source config-zip -n $webappname -g $resourceGroupName --src ".\dist\Host.zip"

Maybe not so easy.  If we run that script the chances are good we'll get some error about an IP restriction:

SqlException: Cannot open server requested by login. Client with IP Address is now allowed to access the server. To enable access use the Windows Azure Management Portal or run sp_set_firewall_rule on the master database to create a firewall rule for this IP address or address range.

Either add the IP to the SQL Firewall, or allow all Azure resources to access it like:

# add firewall rule for access from Azure services
az sql server firewall-rule create -g $resourceGroupName -s $sqlName `
    -n Azure --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0

Problem solved.  Of course, behind every good error there's always another error:

Failed to fetch swagger.json. Possible mixed-content issue? The page was loaded over https:// but a http:// URL was specified. Check that you are not attempting to load mixed content.

This sneaky problem is most likely that ASP.Net Boilerplate depends on knowing the host URL, the Angular URL, and any other possible URL's for CORS.  Because we're going to publish Angular at the same URL we can set all three to the same URL, and per the error above they're currently set to http://localhost:[someport].  At some point we might add a custom URL and custom SSL cert, but for now we can just override them with the azurewebsites.net setting:

# set url's
$url = "https://leesstore2.azurewebsites.net/"
az webapp config appsettings set -n $webappname -g $resourceGroupName `
    --settings `
    App__ServerRootAddress=$url `
    App__ClientRootAddress=$url `
    App__CorsOrigins=$url

And with any luck Swagger! 😁

Swagger UI

5. Compile and Deploy Angular

That's great.  But however much devs might like that UI, users won't exactly be blown away.  We need the Angular site.

Step one is to compile the SPA.  With Angular that's as easy as ng build --prod.  Throwing on an --aot, however, will catch more compiler errors, that's good.  Then let's send the resulting site to a temporary location and move it to the Hosts's /wwwroot folder.

# Compiler angular
Push-Location
Set-Location ".\angular"
ng build --prod --aot --output-path "../dist/ng"
Pop-Location
Move-Item "dist/ng" "aspnet-core/src/LeesStore.Web.Host/wwwroot"

If we run the Host site now, it'll continue to throw up the Swagger site.  That's because the call to app.UseStaticFiles() in Startup.cs excludes default files, like Angular's oh so important index.html.

We could either add app.UseDefaultFiles(new DefaultFilesOptions()); or replace the app.UseStaticFiles() call with app.UseFileServer() and now if we run it locally (remembering to update the App section of appsetting.json) then Angular works at our Host port of 21021!

LeesStore Login Page

Not bad, but there's still two problems.

Problem 1 - appconfig.json

If we rerun the deploy script now things fail on the server:

GetAll net::ERR_CONNECTION_REFUSED

See the issue?  The immediate error and the underlying problem are both in the screenshot.

The immediate error (in the Console) is that the site made a request to http://localhost:21021/.../GetAll.  The underlying problem (in the Network tab's Response window) is that when it made a call to appconfig.production.json, both the remoteServiceBaseUrl and the appBaseUrl were wrong.  

We could fix those problems in a variety of ways, but I especially like adding ASP.Net Core middleware to catch that request and dynamically generate the values based on what's in the appconfig.json (or in environment variables).  Check this out:

public class Startup
{
    ...
    public void Configure(...)
    {
        ...
        app.UseCors(_defaultCorsPolicyName);
        
        app.UseDynamicAppConfig(_appConfiguration);
        
        //app.UseSpa();
        
        app.UseFileServer();
        ...
    }
}

Ha, a setup, didn't see that coming did you?  We just used an undefined UseDynamicAppConfig() extension method in Startup.cs/Configure().  Again, this time for real:

public static class DynamicAppConfigMiddlewareBuilder
{
    public static IApplicationBuilder UseDynamicAppConfig(this IApplicationBuilder builder, IConfigurationRoot appConfiguration)
    {
        var serverRootAddress = appConfiguration["App:ServerRootAddress"];
        var clientRootAddress = appConfiguration["App:ClientRootAddress"];
        return builder.UseMiddleware<DynamicAppConfigMiddleware>(serverRootAddress, clientRootAddress);
    }
}

public class DynamicAppConfigMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _serverRootAddress;
    private readonly string _clientRootAddress;

    public DynamicAppConfigMiddleware(RequestDelegate next, string serverRootAddress, string clientRootAddress)
    {
        _next = next;
        _serverRootAddress = serverRootAddress;
        _clientRootAddress = clientRootAddress;
    }

    public async Task Invoke(HttpContext context)
    {
        const string appConfigPath = "/assets/appconfig.production.json";
        var isRequestingAppConfig = appConfigPath.Equals(context.Request.Path.Value, StringComparison.CurrentCultureIgnoreCase);
        if (isRequestingAppConfig)
        {
            string response = GenerateResponse();
            await context.Response.WriteAsync(response);
        }
        else
        {
            await _next.Invoke(context);
        }
    }

    private string GenerateResponse()
    {
        var cleanServerRootAddress = _serverRootAddress.TrimEnd('/');
        var cleanClientRootAddress = _clientRootAddress.TrimEnd('/');
        return $@"{{
""remoteServiceBaseUrl"": ""{cleanServerRootAddress}"",
""appBaseUrl"": ""{cleanClientRootAddress}""
}}";
    }
}

That looks complicated, but it just intercepts any requests to /assets/appconfig.production.json and writes out a string json in GenerateResponse() that includes values from the appConfiguration.

I love the idea of dynamically generating that config file rather than duplicating values, it feels very clean.  And it even works!

Rerun the deploy script, and holy cow!

Home page for uncustomized ASP.Net Boilerplate Site

We can log in and access the database!

And you know that's exciting because I just ended three paragraphs with exclamation points! (four?  ugh, stupid off by one errors)

Problem 2: Angular Server-Side Routing

But behind any apparent success there is at least ... one minor problem.  In this case if we refresh the page on any URL other than the root we get a 404.  This is a server-side routing problem.  We need any requests to /app/[anything] to get rerouted to /index.html.  Fortunately that's easy to fix with a little more middleware magic.  Check out the app.UseSpa(); call on line 11 of Startup.cs above.  If we uncomment that and add the following middleware:

public static class SpaMiddlewareBuilder
{
    public static void UseSpa(this IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            await next();
            if (context.Response.StatusCode == 404 &&
                context.Request.Path.StartsWithSegments("/app") &&
                !Path.HasExtension(context.Request.Path.Value))
            {
                context.Request.Path = "/index.html";
                await next();
            }
        });
    }
}

Now we can refresh on any page in Angular and it all just works.

πŸŽ‰ Done!

We're done!!  This warrants even more exclamation points!!!

SPA's add deployment complexity, but that deployment was pretty quick and easy.  If you're interested in the source code check out Pull Request 22 on LeesStore on GitHub.  Now we just need to rewrite it all in Cake, use Terraform, and automate those scripts to run on the server.  Tomorrow perhaps.

Was this interesting?  I'd be happy to dig into more complex ABP deployment scenarios, just let me know in the comments or on twitter.