Scott Hanselman

Minimal APIs in .NET 6 but where are the Unit Tests?

September 09, 2021 Comment on this post [0] Posted in DotNetCore | Open Source | Web Services
Sponsored By

imageMinimal APIs in .NET 6 is great. But where are the Unit Tests?! Often testing is missed or forgotten because it's perceived as difficult or complex.

But it's super fun and very easy! Once tests are easy to write, WRITE A LOT OF THEM.

Here's a simple Unit Test of a Web API:

[Fact]
public async Task GetTodos()
{
await using var application = new TodoApplication();

var client = application.CreateClient();
var todos = await client.GetFromJsonAsync<List<Todo>>("/todos");

Assert.Empty(todos);
}

Look how nice that is. Client and Server (Application) are right there, and the HTTP GET is just a function call (as this is a Unit Test, not an integration test that covers end-to-end full stack).

Here's the TodoApplication application factory that creates a Host with a mocked out in memory version of a SQLite database.

class TodoApplication : WebApplicationFactory<Todo>
{
protected override IHost CreateHost(IHostBuilder builder)
{
var root = new InMemoryDatabaseRoot();

builder.ConfigureServices(services =>
{
services.AddScoped(sp =>
{
// Replace SQLite with the in memory provider for tests
return new DbContextOptionsBuilder<TodoDbContext>()
.UseInMemoryDatabase("Tests", root)
.UseApplicationServiceProvider(sp)
.Options;
});
});

return base.CreateHost(builder);
}
}

Nice and clean. You're talking directly to the API, testing just the Unit of Work. No need for HTTP, you're just calling a clean method on the existing API, directly.

That's a simple example, just getting Todos. How would we test making one (POSTing to our Todo application as a Minimal .NET 6 API?)

[Fact]
public async Task PostTodos()
{
await using var application = new TodoApplication();

var client = application.CreateClient();
var response = await client.PostAsJsonAsync("/todos", new Todo { Title = "I want to do this thing tomorrow" });

Assert.Equal(HttpStatusCode.Created, response.StatusCode);

var todos = await client.GetFromJsonAsync<List<Todo>>("/todos");

Assert.Single(todos);
Assert.Equal("I want to do this thing tomorrow", todos[0].Title);
Assert.False(todos[0].IsComplete);
}

You could abstract the setup away if you wanted to and start with an Server/App and Client ready to go, but it's just two lines.

Here we are asserting that it returned an HTTP 200 - even though the HTTP networking stack isn't involved we are still able to test intent. Then we confirm that we created a Todo and could successfully retrieve it from the (in-memory) database.

Pretty slick!


Sponsor: YugabyteDB is a distributed SQL database designed for resilience and scale. It is 100% open source, PostgreSQL-compatible, enterprise-grade, and runs across all clouds. Sign up and get a free t-shirt.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service

Minimal APIs at a glance in .NET 6

September 07, 2021 Comment on this post [14] Posted in DotNetCore | Open Source | Web Services
Sponsored By

imageDavid Fowler doesn't have a blog. I think the psychic weight of having a blog would stress him out. Fortunately, David's 'blog' is actually hidden in his prolific GitHub commits and GitHub Gists.

David has been quietly creating  an amazing piece of documentation for Minimal APIs in .NET 6. At some point when it's released we'll work with David to get everything promoted to formal documentation, but as far as I'm concerned if he is slapping the keyboard anywhere and it shows up anywhere with a URL then I'm happy with the result!

Let's explore a bit here and I encourage you to head over to the main Gist here.

To start, we see how easy it is to make a .NET 6 (minimal) app to say Hello World over HTTP on localhost:5000/5001

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World");

app.Run();

Lovely. It's basically nothing. Can I do more HTTP Verbs? Yes.

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

What about other verbs? More than one?

app.MapMethods("/options-or-head", new [] { "OPTIONS", "HEAD" }, () => "This is an options or head request ");

Lambda expressions, not objects, are our "atoms" that we build molecules with in this world. They are the building blocks.

app.MapGet("/", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler)

But it's just a function, so you can organize things however you want!

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

class HelloHandler
{
public string Hello()
{
return "Hello World";
}
}

You can capture route parameters as part of the route pattern definition.

app.MapGet("/users/{userId}/books/{bookId}", (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

Route constraints are influence the matching behavior of a route. See how this is in order of specificity:

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

Attributes can be used to explicitly declare where parameters should be bound from! So you can pick and choose from all over!

using Microsoft.AspNetCore.Mvc;

app.MapGet("/{id}", ([FromRoute]int id,
[FromQuery(Name = "p")]int page,
[FromServices]Service service,
[FromHeader(Name = "Content-Type")]string contentType) => { });

I can customize the response:

app.MapGet("/todos/{id}", (int id, TodoDb db) => 
db.Todos.Find(id) is Todo todo
? Results.Ok(todo)
: Results.NotFound()
);

Here's a cool example of taking a file upload and writing it to a local file. Nice and clean.

app.MapGet("/upload", async (HttpRequest req) =>
{
if (!req.HasFormContentType)
{
return Results.BadRequest();
}

var form = await req.ReadFormAsync();
var file = form.Files["file"];

if (file is null)
{
return Results.BadRequest();
}

var uploads = Path.Combine(uploadsPath, file.FileName);
await using var fileStream = File.OpenWrite(uploads);
await using var uploadStream = file.OpenReadStream();
await uploadStream.CopyToAsync(fileStream);

return Results.NoContent();
})
.Accepts<IFormFile>("multipart/form-data");

Go check out this great (and growing) online resource to learn about .NET 6 minimal APIs.


Sponsor: YugabyteDB is a distributed SQL database designed for resilience and scale. It is 100% open source, PostgreSQL-compatible, enterprise-grade, and runs across all clouds. Sign up and get a free t-shirt.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service

A .NET 6 Minimal API Todo example Playground

September 02, 2021 Comment on this post [3] Posted in DotNetCore | Open Source | Web Services
Sponsored By

Minimalism from UnsplashI really like minimal Web APIs. I've liked the idea for years. With .NET 6, it's starting to happen! Damian Edwards has an interesting minimal API Playground on his GitHub and Maria Naggaga did a great talk on Minimal APIs in .NET 6 that's up on YouTube!

Let's explore! I'm running the latest .NET 6 and you can run it on Windows, Mac, or Linux and I cloned it to a folder locally.

There's two versions of a complete Todo API in this sample, one using Entity Framework Core and one using Dapper for data access. Both are lightweight ORMs (object relational mappers). Let's explore the Dapper example that uses SQLite.

The opening of the code in this example doesn't require a Main() which removes a nice bit of historically unneeded syntactic sodium. The Main is implied.

using System.ComponentModel.DataAnnotations;
using Microsoft.Data.Sqlite;
using Dapper;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("TodoDb") ?? "Data Source=todos.db";
builder.Services.AddScoped(_ => new SqliteConnection(connectionString));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

At this point we've got a SQLite connection string ready to go scoped in the Services Dependency Injection Container (fancy words for "it's in the pile of stuff we'll be using later") and we've told the system we want a nice UI for our Open API (Swagger) web services description. It's WSDL for JSON, kids!

Then a call to EnsureDb which, ahem, ensures there's a database!

await EnsureDb(app.Services, app.Logger);

What's it look like? Just a little make this table if it doesn't exist action:

async Task EnsureDb(IServiceProvider services, ILogger logger)
{
logger.LogInformation("Ensuring database exists at connection string '{connectionString}'", connectionString);

using var db = services.CreateScope().ServiceProvider.GetRequiredService<SqliteConnection>();
var sql = $@"CREATE TABLE IF NOT EXISTS Todos (
{nameof(Todo.Id)} INTEGER PRIMARY KEY AUTOINCREMENT,
{nameof(Todo.Title)} TEXT NOT NULL,
{nameof(Todo.IsComplete)} INTEGER DEFAULT 0 NOT NULL CHECK({nameof(Todo.IsComplete)} IN (0, 1))
);";
await db.ExecuteAsync(sql);
}

Next we'll "map" some paths for /error as well as paths for our API's UI so when I hit /swagger with a web browser it looks nice:

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error");
}

app.MapGet("/error", () => Results.Problem("An error occurred.", statusCode: 500))
.ExcludeFromDescription();

app.MapSwagger();
app.UseSwaggerUI();

Then sprinkle in a little Hello World just to give folks a taste:

app.MapGet("/", () => "Hello World!")
.WithName("Hello");

app.MapGet("/hello", () => new { Hello = "World" })
.WithName("HelloObject");

You can see how /hello would return a JSON object of Hello: "World"

What's that WithName bit at the end? That names the API and corresponds to 'operationId" in the generated swagger/openAPI json file. It's a shorthand for .WithMetadata(new EndpointNameMetadata("get_product")); which was surely no fun at all.

Now let's get some Todos from this database, shall we? Here's all of them and just the complete ones:

app.MapGet("/todos", async (SqliteConnection db) =>
await db.QueryAsync<Todo>("SELECT * FROM Todos"))
.WithName("GetAllTodos");

app.MapGet("/todos/complete", async (SqliteConnection db) =>
await db.QueryAsync<Todo>("SELECT * FROM Todos WHERE IsComplete = true"))
.WithName("GetCompleteTodos");

Lovely. But what's this Todo object? We haven't seen that. It's just a object that's shaped right. Perhaps one day that could be a record rather than a class but neither Dapper or EFCore support that yet it seems. Still, it's minimal.

public class Todo
{
public int Id { get; set; }
[Required]
public string? Title { get; set; }
public bool IsComplete { get; set; }
}

Let's get a little fancier with an API that gets a Todo but it might not find the result! It may produce an HTTP 200 OK or an HTTP 404 NotFound.

app.MapGet("/todos/{id}", async (int id, SqliteConnection db) =>
await db.QuerySingleOrDefaultAsync<Todo>("SELECT * FROM Todos WHERE Id = @id", new { id })
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.WithName("GetTodoById")
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);

Don't be sad if you don't like SQL like this, it's just a choice amongst many. You can use whatever ORM you want, worry not.

A thought: The .Produces are used by the OpenAPI/Swagger system. In my mind, it'd be nice to avoid saying it twice as the Results.Ok and Results.NotFound is sitting right there, but you'd need a Source Generator or aspect-oriented post compilation weaver to tack in on after the fact. This is the only part that I don't like.

Go explore the code and check it out for yourself!


Check out our Sponsor! YugabyteDB is a distributed SQL database designed for resilience and scale. It is 100% open source, PostgreSQL-compatible, enterprise-grade, and runs across all clouds. Sign up and get a free t-shirt!

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service

My Ultimate PowerShell prompt with Oh My Posh and the Windows Terminal

August 31, 2021 Comment on this post [17] Posted in Open Source | Win10 | Win11
Sponsored By

I've long blogged about my love of setting up a nice terminal, getting the prompt just right, setting my colors, fonts, glyphs, and more. Here's some of my posts.

I want to take a moment to update my pretty prompt post with a little more detail and a more complex PowerShell $PROFILE, due to some changes in Oh My Posh, PowerShell, and the Windows Terminal. I doubt that this post is perfect and I'm sure there's stuff here that is a little extra. But I like it, and this post will serve as my "setting up a new machine" post until I get around to writing a script to do all this for me in one line.

I love my prompt.

A pretty prompt in Terminal with Oh My Posh and a lot of Colors

Let's get you set up!

Get PowerShell

I don't mean Windows PowerShell (that's "classic" now) I mean the .NET Core-powered cross-platform PowerShell. There's a LOT of ways to get it but I'm a Store person usually so I can get PowerShell (and it'll auto update) from the Microsoft Store or just "winget install Microsoft.PowerShell" from the command line with winget.

Get Windows Terminal and set a default Shell

Get Windows Terminal if you don't already have it, you can get Windows Terminal free from the Store. If you don't have access to the Microsoft Store, the builds are published on the GitHub releases page. It comes with a lovely font called Cascadia Code...but...

Now that you have Windows Terminal, you'll notice that it knows that you have PowerShell installed and will add it to your Windows Terminal dropdown menu! You can set PowerShell as your default Profile - that's the one you'll get by default when you make a new Tab - in settings:

Set your Default Profile to PowerShell

Upgrade your Terminal/Console Fonts

I like fonts with lots of Glyphs so I also download and Install Caskaydia Cove Nerd Font Complete. This is the same Cascadia Code font but MODIFIED to include hundreds of special characters that you can use to make your prompt cooler.

IMPORTANT NOTE: The string literal name of this font for use in settings or VS Code is "CaskaydiaCove NF". If you're using Cascadia Code, there are different strings for each. The NUMBER ONE question I get is 'why don't my glyphs/fonts show up right in Windows Terminal/VS Code?' and the answer is almost always "you're using the wrong font string." It's usually either an extra space or a missing space, so don't be afraid to double check.
Cascadia Code

Remember that Windows Terminal has a lovely Settings UI but you can always click "open JSON file" to manage the settings.json as text if you prefer. Here's mine. Yours will be different and you should customize it! The Windows Terminal documentation is fantastic. Again, see how PowerShell is in BOLD? That's because it's my default.

Lots of Windows Terminal Profiles

Now, let's add a little...spice...

Add "Oh My Posh" to your Shell

Oh My Posh has amazing docs so check them out. Do note that some stuff has changed, especially from v2 to v3.

EXCITING NOTE: Oh My Posh is portable and works on any shell, so I use it on both my "Pwsh" (PowerShell) in Windows and my Bash shells on WSL running Ubuntu.

You can install Oh My Posh with with PowerShell's "Install-Module" or with the platform-specific install instructions. I used the latter, which is somewhat new, but it's tomato/tomato, so use what works for you.

Again, read the docs but the idea on Windows is basically this (or get it from GitHub):

winget install JanDeDobbeleer.OhMyPosh
# restart shell to reload PATH

Then edit $PROFILE and add the following line, remembering at this point that oh-my-posh is an executable on the PATH.

oh-my-posh --init --shell pwsh --config ~/jandedobbeleer.omp.json | Invoke-Expression

I have changed my Oh My Posh from Jan's default to include my own stuff, and I keep my latest up in a GitHub Gist and also in my DropBox/OneDrive so it's always syncing to all my machines. Mine is this, after I download from my gist.

oh-my-posh --init --shell pwsh --config D:/Dropbox/ohmyposhv3-2.json | Invoke-Expression

Yours will vary. Again, read the docs and experiment! Once added, reload your profile for the changes to take effect, or restart your shell.

. $PROFILE

That .json file is filled with "segments" that are documented on the Oh My Posh site in a lot of detail. Overwhelming detail. You can add your computer's battery, your Azure Subscription, the dotnet or node version of your current folder, really anything. Even your Spotify songs. I'm going to make one that show my Blood Sugar.

Go explore Oh My Posh Themes and then modify them with your own additional Segments.

Again, note that your fonts will need the right glyphs or it will look weird.

Here's a GOOD prompt:

This prompt has nice glyphs

Here's a BAD prompt with an issue!

This prompt has weird squares

Why is it wrong? Either the .json file that is your config has been saved wrong or corrupted the Unicode Glyphs, or you've got a font that doesn't have those glyphs.

Re-assert your Git segment in Oh My Posh

Some folks want full git info, status, added, modified, untracked, etc and others just want the current git branch. Check the Git segment and the Posh Git segment to make sure you are getting the performance AND information you need.

I needed to turn on "display_stash_count" and "display_upstream_icon" in my config json, like this:

{
"type": "git",
"style": "powerline",
"powerline_symbol": "",
"invert_powerline": false,
"foreground": "#193549",
"background": "#fffb38",
"leading_diamond": "",
"trailing_diamond": "",
"properties": {
"display_status": true,
"display_stash_count": true,
"display_upstream_icon": true
}
},

Again, this is all optional and affect performance slightly, but be aware of these properties. I believe I have these set the right way I want them in my public gist. Here is me moving around my source code with "z" in stead of cd, but note the prompt changes.

My prompt shows my Git branch

Turn your PowerShell directories up to 11 with Terminal-Icons

Is your prompt not extra enough? That's because your directory listing needs color AND cool icons!

Install-Module -Name Terminal-Icons -Repository PSGallery

And then add one line to my $profile (edit with "code $profile"):

Import-Module -Name Terminal-Icons

Sweet!

Cool Icons and color in DIR

How far is too far?

At this point you're basically done, but I also LOVE PSReadLine. It's great generally but also nice for bash and Emacs types who are moving to PowerShell or use PowerShell for work.

I've added features like "ctrl shift b" at the command line will run "dotnet build." Why? Because I can and because it's muscle memory so I'm making my prompt work for me.

You can also add Predictive Autocomplete to your prompt if you like but I'll leave that as an exercise to the reader! My PowerShell profile is on a public gist, and while it's not perfect and likely has issues, it works for me!

Enjoy! Thanks to the Windows Terminal Team and the always lovely Jan De Dobbeleer from Oh My Posh, as well as Brandon Olin from Terminal Icons.


Check out our Sponsor! YugabyteDB is a distributed SQL database designed for resilience and scale. It is 100% open source, PostgreSQL-compatible, enterprise-grade, and runs across all clouds. Sign up and get a free t-shirt!

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service

Hanselminutes is Fresh Air for Developers and has over 800 episodes of fresh tech from fresh faces

August 22, 2021 Comment on this post [7] Posted in
Sponsored By

Hanselminutes PodcastHey friends! I wanted remind you about my podcast! It's http://hanselminutes.com/ and just a few weeks ago I published my 800th episode! My first episode was in January of 2006 so that's over 15 years of shows. And, if I may be a little boastful for a moment, they are pretty darn good. Maybe the first 400 were a little rough but these last 400 have been ROCK SOLID. Just kidding.

Seriously, though, this 30 minute long tech show has diverse topics and new faces you haven't heard on other podcasts. If you check out over 800 episodes here https://www.hanselminutes.com/episodes you can search by Title, Guest, OR search all the Transcripts! There's over 400 hours of shows and you can search for the topics you want.

Subscribe with your favorite podcast app, the raw RSS is here. We're also available on basically every podcast app out there, including, but not limited to:

If you enjoy the show, the best thing you can do to help me is SPREAD THE WORD! Tell a friend, share and episode or favorite code, but above all GET FOLKS TO SUBSCRIBE.

The world is littered with podcasts that gave up after 9 episodes. There's a ton of average talks shows that ramble on. I've worked really hard - at night, as this is not my day job! - to not only bring you the best guests, but to read their papers, books, and thesis, and ask the questions that YOU would have if you were here with me!

Sometimes I even put the Hanselminutes Podcast on YouTube and the results are truly special and heartbreakingly emotional.

Thanks for listening, and thanks for sharing!


Sponsor: YugabyteDB is a distributed SQL database designed for resilience and scale. It is 100% open source, PostgreSQL-compatible, enterprise-grade, and runs across all clouds. Sign up and get a free t-shirt

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.